From d25d296498d0a74413da6209894150622dc24287 Mon Sep 17 00:00:00 2001 From: adriano Date: Wed, 21 Feb 2024 18:10:30 -0300 Subject: [PATCH 01/10] feat: Update response message for clarity --- backend/src/controllers/TicketController.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index 3c1c930..38b4218 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -192,7 +192,7 @@ export const remoteTicketCreation = async ( ticketId: ticket.id }); } - } + } if (!ticket) { ticket = await FindOrCreateTicketService( @@ -226,11 +226,13 @@ export const remoteTicketCreation = async ( } console.log( - `REMOTE TICKET CREATION FROM ENDPOINT | STATUS: 500 | MSG: Whatsapp number ${contact_from} disconnected` + `REMOTE TICKET CREATION FROM ENDPOINT | STATUS: 500 | MSG: Whatsapp number ${contact_from} disconnected or it doesn't exist in omnihit` ); return res .status(500) - .json({ msg: `Whatsapp number ${contact_from} disconnected` }); + .json({ + msg: `Whatsapp number ${contact_from} disconnected or it doesn't exist in omnihit` + }); }; export const store = async (req: Request, res: Response): Promise => { From 7fc0f136ffc625b738c91567284fb2d14defdb9a Mon Sep 17 00:00:00 2001 From: adriano Date: Thu, 22 Feb 2024 15:20:26 -0300 Subject: [PATCH 02/10] fix: Fix bug in ticket transfer from selected user's queue by agent --- backend/src/controllers/UserController.ts | 9 ++++++--- .../UserServices/ListUserParamiterService.ts | 12 +++++++++--- frontend/src/components/TransferTicketModal/index.js | 2 ++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/backend/src/controllers/UserController.ts b/backend/src/controllers/UserController.ts index d36b7ff..88d463a 100644 --- a/backend/src/controllers/UserController.ts +++ b/backend/src/controllers/UserController.ts @@ -99,7 +99,7 @@ export const index = async (req: Request, res: Response): Promise => { // }; export const all = async (req: Request, res: Response): Promise => { - const { userId, profile } = req.query as IndexQuery; + let { userId, profile }: any = req.query as IndexQuery; console.log( "userId: ", @@ -110,7 +110,10 @@ export const all = async (req: Request, res: Response): Promise => { getSettingValue("queueTransferByWhatsappScope")?.value ); - if (getSettingValue("queueTransferByWhatsappScope")?.value == "enabled") { + if (getSettingValue("queueTransferByWhatsappScope")?.value == "enabled") { + + if (!userId) return res.json({ users: [], queues: [] }); + const obj = await ListUserByWhatsappQueuesService( userId, '"admin", "user", "supervisor"' @@ -119,7 +122,7 @@ export const all = async (req: Request, res: Response): Promise => { const usersByWhatsqueue = obj.users; const queues = obj.queues; - let userIds = usersByWhatsqueue.map((w: any) => w.userId); + let userIds = usersByWhatsqueue.map((w: any) => w.userId); const users = await ListUser({ userIds diff --git a/backend/src/services/UserServices/ListUserParamiterService.ts b/backend/src/services/UserServices/ListUserParamiterService.ts index ebb610b..2d92498 100644 --- a/backend/src/services/UserServices/ListUserParamiterService.ts +++ b/backend/src/services/UserServices/ListUserParamiterService.ts @@ -2,7 +2,7 @@ import { Op, Sequelize } from "sequelize"; import Queue from "../../models/Queue"; import User from "../../models/User"; import UserQueue from "../../models/UserQueue"; -import { List } from "whatsapp-web.js" +import { List } from "whatsapp-web.js"; interface Request { userId?: string | number; @@ -12,7 +12,13 @@ interface Request { userIds?: string | number; } -const ListUser = async ({ profile, userId, raw, userIds, profiles }: Request): Promise => { +const ListUser = async ({ + profile, + userId, + raw, + userIds, + profiles +}: Request): Promise => { let where_clause = {}; if (userId && profile) { @@ -47,7 +53,7 @@ const ListUser = async ({ profile, userId, raw, userIds, profiles }: Request): P ], order: [["id", "ASC"]], - group: ["User.id"] + group: userIds ? undefined : ["User.id"] }); return users; diff --git a/frontend/src/components/TransferTicketModal/index.js b/frontend/src/components/TransferTicketModal/index.js index a658483..ad79ea4 100644 --- a/frontend/src/components/TransferTicketModal/index.js +++ b/frontend/src/components/TransferTicketModal/index.js @@ -195,6 +195,8 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => { params: { userId: user.id }, }) + console.log('data.queues: ', data.queues, ' | data.users: ', data.users) + setUsers(data.users) setQueuesByWhats(data.queues) setQueues(data.queues) From 05dd0e60c0f8ed4c9aefeb8cfc05ab983d6601c5 Mon Sep 17 00:00:00 2001 From: adriano Date: Thu, 22 Feb 2024 15:30:43 -0300 Subject: [PATCH 03/10] chore: Remove frontend comment --- frontend/src/components/TransferTicketModal/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/components/TransferTicketModal/index.js b/frontend/src/components/TransferTicketModal/index.js index ad79ea4..472565b 100644 --- a/frontend/src/components/TransferTicketModal/index.js +++ b/frontend/src/components/TransferTicketModal/index.js @@ -194,9 +194,7 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid }) => { const { data } = await api.get(`/users/all`, { params: { userId: user.id }, }) - - console.log('data.queues: ', data.queues, ' | data.users: ', data.users) - + setUsers(data.users) setQueuesByWhats(data.queues) setQueues(data.queues) From 1d3a6178baed3eec40d4d203c2b5f86c5c9ea253 Mon Sep 17 00:00:00 2001 From: adriano Date: Fri, 23 Feb 2024 14:36:52 -0300 Subject: [PATCH 04/10] feat: Improve code for transferring users to another agent --- backend/src/controllers/TicketController.ts | 9 ++++ .../WbotServices/wbotMessageListener.ts | 52 +++++++++++++------ 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/backend/src/controllers/TicketController.ts b/backend/src/controllers/TicketController.ts index 38b4218..bf63ec2 100644 --- a/backend/src/controllers/TicketController.ts +++ b/backend/src/controllers/TicketController.ts @@ -120,12 +120,21 @@ export const remoteTicketCreation = async ( const { contact_from, contact_to, msg, contact_name }: any = req.body; const validate = ["contact_from", "contact_to", "msg"]; + const validateOnlyNumber = ["contact_from", "contact_to"]; for (let prop of validate) { if (!req.body[prop]) return res .status(400) .json({ error: `Property '${prop}' is undefined.` }); + + if(validateOnlyNumber.includes(prop)){ + if(!(/^\d+$/.test(req.body[prop]))){ + return res + .status(400) + .json({ error: `The property '${prop}' must be a number` }); + } + } } const whatsapp = await Whatsapp.findOne({ diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index d0b1a8f..55f0552 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -497,7 +497,12 @@ const queuesOutBot = async (wbot: Session, botId: string | number) => { return { queues, greetingMessage }; }; -const transferTicket = async (queueName: any, wbot: any, ticket: Ticket) => { +const transferTicket = async ( + queueName: any, + wbot: any, + ticket: Ticket, + sendGreetingMessage?: boolean +) => { const botInfo = await BotIsOnQueue("botqueue"); console.log("kkkkkkkkkkkkkkkkkkkkk queueName: ", queueName); @@ -519,16 +524,24 @@ const transferTicket = async (queueName: any, wbot: any, ticket: Ticket) => { queue = queues[queueName]; } - if (queue) await botTransferTicket(queue, ticket); + if (queue) await botTransferTicket(queue, ticket, sendGreetingMessage); }; -const botTransferTicket = async (queues: Queue, ticket: Ticket) => { +const botTransferTicket = async ( + queues: Queue, + ticket: Ticket, + sendGreetingMessage?: boolean +) => { await ticket.update({ userId: null }); await UpdateTicketService({ ticketData: { status: "pending", queueId: queues.id }, ticketId: ticket.id }); + + if (sendGreetingMessage && queues?.greetingMessage?.length > 0) { + botSendMessage(ticket, queues.greetingMessage); + } }; const botTransferTicketToUser = async ( @@ -759,12 +772,28 @@ const handleMessage = async ( await verifyQueue(wbot, msg, ticket, contact); } + const botInfo = await BotIsOnQueue("botqueue"); + // Transfer to agent - if (!msg.fromMe) { - const filteredUsers = await findByContain("user:*", "name", msg?.body); - + // O bot interage com o cliente e encaminha o atendimento para fila de atendende quando o usuário escolhe a opção falar com atendente + if ( + !msg.fromMe && + ((ticket.status == "open" && + botInfo && + ticket.userId == +botInfo.userIdBot) || + ticket.status == "pending" || + ticket.status == "queueChoice") + ) { + const filteredUsers = await findByContain("user:*", "name", msg?.body); + + console.log("######## filteredUsers: ", filteredUsers); if (filteredUsers && filteredUsers.length > 0) { + if (botInfo.isOnQueue) { + transferTicket(filteredUsers[0].name, wbot, ticket, true); + return; + } + const whatsappQueues = await ListWhatsappQueuesByUserQueue( +filteredUsers[0].id ); @@ -791,12 +820,6 @@ const handleMessage = async ( } // - // O bot interage com o cliente e encaminha o atendimento para fila de atendende quando o usuário escolhe a opção falar com atendente - - //Habilitar esse caso queira usar o bot - const botInfo = await BotIsOnQueue("botqueue"); - // const botInfo = { isOnQueue: false, botQueueId: 0, userIdBot: 0 }; - if ( botInfo.isOnQueue && !msg.fromMe && @@ -844,11 +867,6 @@ const handleMessage = async ( menuMsg?.transferToQueue && menuMsg.transferToQueue.trim().length > 0 ) { - console.log( - "YYYYYYYYYYYYYYYYYYYY menuMsg.transferToQueue: ", - menuMsg.transferToQueue - ); - transferTicket(menuMsg.transferToQueue.trim(), wbot, ticket); } } From 237fa8124e940ff847460073fc9488fc632d0bab Mon Sep 17 00:00:00 2001 From: adriano Date: Mon, 26 Feb 2024 11:18:01 -0300 Subject: [PATCH 05/10] feat: Update application to receive vecard from the client --- backend/src/controllers/ContactController.ts | 20 ++ .../src/helpers/CheckContactOpenTickets.ts | 4 +- backend/src/routes/contactRoutes.ts | 2 + .../ContactServices/GetContactService.ts | 38 ++++ .../WbotServices/wbotMessageListener.ts | 44 +++- frontend/src/components/Audio/index.jsx | 62 +++++ .../src/components/LocationPreview/index.js | 53 +++++ .../src/components/MarkdownWrapper/index.js | 43 ++-- frontend/src/components/MessagesList/index.js | 213 ++++++++++++++++-- frontend/src/components/VcardPreview/index.js | 94 ++++++++ 10 files changed, 528 insertions(+), 45 deletions(-) create mode 100644 backend/src/services/ContactServices/GetContactService.ts create mode 100644 frontend/src/components/Audio/index.jsx create mode 100644 frontend/src/components/LocationPreview/index.js create mode 100644 frontend/src/components/VcardPreview/index.js diff --git a/backend/src/controllers/ContactController.ts b/backend/src/controllers/ContactController.ts index 7ab9363..1b194a1 100644 --- a/backend/src/controllers/ContactController.ts +++ b/backend/src/controllers/ContactController.ts @@ -20,12 +20,18 @@ import { } from "../helpers/ContactsCache"; import { off } from "process"; +import GetContactService from "../services/ContactServices/GetContactService" type IndexQuery = { searchParam: string; pageNumber: string; }; +type IndexGetContactQuery = { + name: string; + number: string; +}; + interface ExtraInfo { name: string; value: string; @@ -84,6 +90,20 @@ export const index = async (req: Request, res: Response): Promise => { return res.json({ contacts, count, hasMore }); }; +export const getContact = async ( + req: Request, + res: Response +): Promise => { + const { name, number } = req.body as IndexGetContactQuery; + + const contact = await GetContactService({ + name, + number + }); + + return res.status(200).json(contact); +}; + export const store = async (req: Request, res: Response): Promise => { const newContact: ContactData = req.body; newContact.number = newContact.number.replace("-", "").replace(" ", ""); diff --git a/backend/src/helpers/CheckContactOpenTickets.ts b/backend/src/helpers/CheckContactOpenTickets.ts index 6cfc24d..303f4a7 100644 --- a/backend/src/helpers/CheckContactOpenTickets.ts +++ b/backend/src/helpers/CheckContactOpenTickets.ts @@ -15,9 +15,9 @@ const CheckContactOpenTickets = async ( if (getSettingValue("oneContactChatWithManyWhats")?.value == "enabled") { let whats = await ListWhatsAppsNumber(whatsappId); - console.log("contactId: ", contactId, " | whatsappId: ", whatsappId); + // console.log("contactId: ", contactId, " | whatsappId: ", whatsappId); - console.log("WHATS: ", JSON.stringify(whats, null, 6)); + // console.log("WHATS: ", JSON.stringify(whats, null, 6)); ticket = await Ticket.findOne({ where: { diff --git a/backend/src/routes/contactRoutes.ts b/backend/src/routes/contactRoutes.ts index 158159c..8c77791 100644 --- a/backend/src/routes/contactRoutes.ts +++ b/backend/src/routes/contactRoutes.ts @@ -16,6 +16,8 @@ contactRoutes.get("/contacts/:contactId", isAuth, ContactController.show); contactRoutes.post("/contacts", isAuth, ContactController.store); +contactRoutes.post("/contact", isAuth, ContactController.getContact); + contactRoutes.put("/contacts/:contactId", isAuth, ContactController.update); contactRoutes.delete("/contacts/:contactId", isAuth, ContactController.remove); diff --git a/backend/src/services/ContactServices/GetContactService.ts b/backend/src/services/ContactServices/GetContactService.ts new file mode 100644 index 0000000..3865bc5 --- /dev/null +++ b/backend/src/services/ContactServices/GetContactService.ts @@ -0,0 +1,38 @@ +import AppError from "../../errors/AppError"; +import Contact from "../../models/Contact"; +import CreateContactService from "./CreateContactService"; + +interface ExtraInfo { + name: string; + value: string; +} + +interface Request { + name: string; + number: string; + email?: string; + profilePicUrl?: string; + extraInfo?: ExtraInfo[]; +} + +const GetContactService = async ({ name, number }: Request): Promise => { + const numberExists = await Contact.findOne({ + where: { number } + }); + + if (!numberExists) { + const contact = await CreateContactService({ + name, + number, + }) + + if (contact == null) + throw new AppError("CONTACT_NOT_FIND") + else + return contact + } + + return numberExists +}; + +export default GetContactService; \ No newline at end of file diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 55f0552..bb1c57e 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -98,6 +98,7 @@ import FindOrCreateTicketServiceBot from "../TicketServices/FindOrCreateTicketSe import ShowTicketService from "../TicketServices/ShowTicketService"; import ShowQueuesByUser from "../UserServices/ShowQueuesByUser"; import ListWhatsappQueuesByUserQueue from "../UserServices/ListWhatsappQueuesByUserQueue"; +import CreateContactService from "../ContactServices/CreateContactService" var lst: any[] = getWhatsappIds(); @@ -479,6 +480,7 @@ const isValidMsg = (msg: any): boolean => { msg.type === "image" || msg.type === "document" || msg.type === "vcard" || + // msg.type === "multi_vcard" || msg.type === "sticker" ) return true; @@ -608,7 +610,7 @@ const handleMessage = async ( return; } - } + } if (!isValidMsg(msg)) { return; @@ -643,7 +645,13 @@ const handleMessage = async ( // media messages sent from me from cell phone, first comes with "hasMedia = false" and type = "image/ptt/etc" // in this case, return and let this message be handled by "media_uploaded" event, when it will have "hasMedia = true" - if (!msg.hasMedia && msg.type !== "chat" && msg.type !== "vcard") return; + if ( + !msg.hasMedia && + msg.type !== "chat" && + msg.type !== "vcard" && + msg.type !== "multi_vcard" + ) + return; } else { console.log(`\n <<<<<<<<<< RECEIVING MESSAGE: Parcial msg and msgContact info: @@ -772,6 +780,34 @@ const handleMessage = async ( await verifyQueue(wbot, msg, ticket, contact); } + if (msg.type === "vcard") { + try { + const array = msg.body.split("\n"); + const obj = []; + let contact = ""; + for (let index = 0; index < array.length; index++) { + const v = array[index]; + const values = v.split(":"); + for (let ind = 0; ind < values.length; ind++) { + if (values[ind].indexOf("+") !== -1) { + obj.push({ number: values[ind] }); + } + if (values[ind].indexOf("FN") !== -1) { + contact = values[ind + 1]; + } + } + } + for await (const ob of obj) { + const cont = await CreateContactService({ + name: contact, + number: ob.number.replace(/\D/g, "") + }); + } + } catch (error) { + console.log(error); + } + } + const botInfo = await BotIsOnQueue("botqueue"); // Transfer to agent @@ -784,9 +820,7 @@ const handleMessage = async ( ticket.status == "pending" || ticket.status == "queueChoice") ) { - const filteredUsers = await findByContain("user:*", "name", msg?.body); - - console.log("######## filteredUsers: ", filteredUsers); + const filteredUsers = await findByContain("user:*", "name", msg?.body); if (filteredUsers && filteredUsers.length > 0) { if (botInfo.isOnQueue) { diff --git a/frontend/src/components/Audio/index.jsx b/frontend/src/components/Audio/index.jsx new file mode 100644 index 0000000..cc0c7cc --- /dev/null +++ b/frontend/src/components/Audio/index.jsx @@ -0,0 +1,62 @@ +import { Button } from "@material-ui/core"; +import React, { useRef } from "react"; +import { useEffect } from "react"; +import { useState } from "react"; + +const LS_NAME = 'audioMessageRate'; + +export default function({url}) { + const audioRef = useRef(null); + const [audioRate, setAudioRate] = useState( parseFloat(localStorage.getItem(LS_NAME) || "1") ); + const [showButtonRate, setShowButtonRate] = useState(false); + + useEffect(() => { + audioRef.current.playbackRate = audioRate; + localStorage.setItem(LS_NAME, audioRate); + }, [audioRate]); + + useEffect(() => { + audioRef.current.onplaying = () => { + setShowButtonRate(true); + }; + audioRef.current.onpause = () => { + setShowButtonRate(false); + }; + audioRef.current.onended = () => { + setShowButtonRate(false); + }; + }, []); + + const toogleRate = () => { + let newRate = null; + + switch(audioRate) { + case 0.5: + newRate = 1; + break; + case 1: + newRate = 1.5; + break; + case 1.5: + newRate = 2; + break; + case 2: + newRate = 0.5; + break; + default: + newRate = 1; + break; + } + + setAudioRate(newRate); + }; + + return ( + <> + + {showButtonRate && } + + ); +} \ No newline at end of file diff --git a/frontend/src/components/LocationPreview/index.js b/frontend/src/components/LocationPreview/index.js new file mode 100644 index 0000000..9187faf --- /dev/null +++ b/frontend/src/components/LocationPreview/index.js @@ -0,0 +1,53 @@ +import React, { useEffect } from 'react'; +import toastError from "../../errors/toastError"; + +import Typography from "@material-ui/core/Typography"; +import Grid from "@material-ui/core/Grid"; + +import { Button, Divider, } from "@material-ui/core"; + +const LocationPreview = ({ image, link, description }) => { + useEffect(() => {}, [image, link, description]); + + const handleLocation = async() => { + try { + window.open(link); + } catch (err) { + toastError(err); + } + } + + return ( + <> +
+
+
+ +
+ { description && ( +
+ +
') }}>
+
+
+ )} +
+
+ + +
+
+
+ + ); + +}; + +export default LocationPreview; \ No newline at end of file diff --git a/frontend/src/components/MarkdownWrapper/index.js b/frontend/src/components/MarkdownWrapper/index.js index 64764b2..3d2c01b 100644 --- a/frontend/src/components/MarkdownWrapper/index.js +++ b/frontend/src/components/MarkdownWrapper/index.js @@ -1,5 +1,5 @@ -import React from "react"; -import Markdown from "markdown-to-jsx"; +import React from "react" +import Markdown from "markdown-to-jsx" const elements = [ "a", @@ -139,25 +139,32 @@ const elements = [ "svg", "text", "tspan", -]; +] -const allowedElements = ["a", "b", "strong", "em", "u", "code", "del"]; +const allowedElements = ["a", "b", "strong", "em", "u", "code", "del"] const CustomLink = ({ children, ...props }) => ( {children} -); +) const MarkdownWrapper = ({ children }) => { - const boldRegex = /\*(.*?)\*/g; - const tildaRegex = /~(.*?)~/g; + const boldRegex = /\*(.*?)\*/g + const tildaRegex = /~(.*?)~/g + + if (children && children.includes('BEGIN:VCARD')) + //children = "Diga olá ao seu novo contato clicando em *conversar*!"; + children = null + + if (children && children.includes('data:image/')) + children = null if (children && boldRegex.test(children)) { - children = children.replace(boldRegex, "**$1**"); + children = children.replace(boldRegex, "**$1**") } if (children && tildaRegex.test(children)) { - children = children.replace(tildaRegex, "~~$1~~"); + children = children.replace(tildaRegex, "~~$1~~") } const options = React.useMemo(() => { @@ -167,20 +174,20 @@ const MarkdownWrapper = ({ children }) => { overrides: { a: { component: CustomLink }, }, - }; + } elements.forEach(element => { if (!allowedElements.includes(element)) { - markdownOptions.overrides[element] = el => el.children || null; + markdownOptions.overrides[element] = el => el.children || null } - }); + }) - return markdownOptions; - }, []); + return markdownOptions + }, []) - if (!children) return null; + if (!children) return null - return {children}; -}; + return {children} +} -export default MarkdownWrapper; +export default MarkdownWrapper \ No newline at end of file diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js index 236601a..9d25471 100644 --- a/frontend/src/components/MessagesList/index.js +++ b/frontend/src/components/MessagesList/index.js @@ -23,6 +23,11 @@ import { } from "@material-ui/icons"; import MarkdownWrapper from "../MarkdownWrapper"; +import VcardPreview from "../VcardPreview"; +import LocationPreview from "../LocationPreview"; +import Audio from "../Audio"; + + import ModalImageCors from "../ModalImageCors"; import MessageOptionsMenu from "../MessageOptionsMenu"; import whatsBackground from "../../assets/wa-background.png"; @@ -488,27 +493,109 @@ const MessagesList = ({ ticketId, isGroup }) => { setAnchorEl(null); }; + // const checkMessageMedia = (message) => { + // if (message.mediaType === "image") { + // return ; + // } + // if (message.mediaType === "audio") { + + // return ( + // + // ); + // } + + // if (message.mediaType === "video") { + // return ( + //