From c8c8dc43e9fcbf02ddc4239a811f6913355ddc9f Mon Sep 17 00:00:00 2001 From: canove Date: Thu, 7 Jan 2021 22:01:13 -0300 Subject: [PATCH] improvement: code cleanup --- ...108001431-add-unreadMessages-to-tickets.ts | 13 + .../src/helpers/SetTicketMessagesAsRead.ts | 2 + backend/src/libs/wbot.ts | 46 +-- backend/src/models/Ticket.ts | 25 +- .../CreateOrUpdateContactService.ts | 59 ++++ .../MessageServices/CreateMessageService.ts | 28 +- .../FindOrCreateTicketService.ts | 81 +++++ .../WbotServices/wbotMessageListener.ts | 289 ++++++------------ .../src/services/WbotServices/wbotMonitor.ts | 4 +- 9 files changed, 297 insertions(+), 250 deletions(-) create mode 100644 backend/src/database/migrations/20210108001431-add-unreadMessages-to-tickets.ts create mode 100644 backend/src/services/ContactServices/CreateOrUpdateContactService.ts create mode 100644 backend/src/services/TicketServices/FindOrCreateTicketService.ts diff --git a/backend/src/database/migrations/20210108001431-add-unreadMessages-to-tickets.ts b/backend/src/database/migrations/20210108001431-add-unreadMessages-to-tickets.ts new file mode 100644 index 0000000..ca5b47f --- /dev/null +++ b/backend/src/database/migrations/20210108001431-add-unreadMessages-to-tickets.ts @@ -0,0 +1,13 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Tickets", "unreadMessages", { + type: DataTypes.INTEGER + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Tickets", "unreadMessages"); + } +}; diff --git a/backend/src/helpers/SetTicketMessagesAsRead.ts b/backend/src/helpers/SetTicketMessagesAsRead.ts index 6fb07e9..6a43f22 100644 --- a/backend/src/helpers/SetTicketMessagesAsRead.ts +++ b/backend/src/helpers/SetTicketMessagesAsRead.ts @@ -15,6 +15,8 @@ const SetTicketMessagesAsRead = async (ticket: Ticket): Promise => { } ); + await ticket.update({ unreadMessages: 0 }); + try { const wbot = await GetTicketWbot(ticket); wbot.sendSeen(`${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`); diff --git a/backend/src/libs/wbot.ts b/backend/src/libs/wbot.ts index e85f0a3..d3fd620 100644 --- a/backend/src/libs/wbot.ts +++ b/backend/src/libs/wbot.ts @@ -4,7 +4,7 @@ import { getIO } from "./socket"; import Whatsapp from "../models/Whatsapp"; import AppError from "../errors/AppError"; import { logger } from "../utils/logger"; -// import { handleMessage } from "../services/WbotServices/wbotMessageListener"; +import { handleMessage } from "../services/WbotServices/wbotMessageListener"; interface Session extends Client { id?: number; @@ -12,20 +12,25 @@ interface Session extends Client { const sessions: Session[] = []; -// const syncUnreadMessages = async (wbot: Session) => { -// const chats = await wbot.getChats(); +const syncUnreadMessages = async (wbot: Session) => { + const chats = await wbot.getChats(); -// chats.forEach(async chat => { -// if (chat.unreadCount > 0) { -// const unreadMessages = await chat.fetchMessages({ -// limit: chat.unreadCount -// }); -// unreadMessages.forEach(msg => { -// handleMessage(msg, wbot); -// }); -// } -// }); -// }; + /* eslint-disable no-restricted-syntax */ + /* eslint-disable no-await-in-loop */ + for (const chat of chats) { + if (chat.unreadCount > 0) { + const unreadMessages = await chat.fetchMessages({ + limit: chat.unreadCount + }); + + for (const msg of unreadMessages) { + await handleMessage(msg, wbot); + } + + await chat.sendSeen(); + } + } +}; export const initWbot = async (whatsapp: Whatsapp): Promise => { return new Promise((resolve, reject) => { @@ -76,14 +81,16 @@ export const initWbot = async (whatsapp: Whatsapp): Promise => { }); wbot.on("authenticated", async session => { - logger.info("Session:", sessionName, "AUTHENTICATED"); + logger.info(`Session: ${sessionName} AUTHENTICATED`); await whatsapp.update({ session: JSON.stringify(session) }); }); wbot.on("auth_failure", async msg => { - console.error("Session:", sessionName, "AUTHENTICATION FAILURE", msg); + console.error( + `Session: ${sessionName} AUTHENTICATION FAILURE! Reason: ${msg}` + ); if (whatsapp.retries > 1) { await whatsapp.update({ session: "", retries: 0 }); @@ -106,8 +113,6 @@ export const initWbot = async (whatsapp: Whatsapp): Promise => { wbot.on("ready", async () => { logger.info("Session:", sessionName, "READY"); - // syncUnreadMessages(wbot); - await whatsapp.update({ status: "CONNECTED", qrcode: "", @@ -119,14 +124,15 @@ export const initWbot = async (whatsapp: Whatsapp): Promise => { session: whatsapp }); - wbot.sendPresenceAvailable(); - const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id); if (sessionIndex === -1) { wbot.id = whatsapp.id; sessions.push(wbot); } + wbot.sendPresenceAvailable(); + syncUnreadMessages(wbot); + resolve(wbot); }); } catch (err) { diff --git a/backend/src/models/Ticket.ts b/backend/src/models/Ticket.ts index 9a07a0a..80d94cf 100644 --- a/backend/src/models/Ticket.ts +++ b/backend/src/models/Ticket.ts @@ -4,14 +4,11 @@ import { CreatedAt, UpdatedAt, Model, - DataType, PrimaryKey, ForeignKey, BelongsTo, HasMany, AutoIncrement, - AfterFind, - BeforeUpdate, Default } from "sequelize-typescript"; @@ -30,7 +27,7 @@ class Ticket extends Model { @Column({ defaultValue: "pending" }) status: string; - @Column(DataType.VIRTUAL) + @Column unreadMessages: number; @Column @@ -69,26 +66,6 @@ class Ticket extends Model { @HasMany(() => Message) messages: Message[]; - - @AfterFind - static async countTicketsUnreadMessages(tickets: Ticket[]): Promise { - if (tickets && tickets.length > 0) { - await Promise.all( - tickets.map(async ticket => { - ticket.unreadMessages = await Message.count({ - where: { ticketId: ticket.id, read: false } - }); - }) - ); - } - } - - @BeforeUpdate - static async countTicketUnreadMessags(ticket: Ticket): Promise { - ticket.unreadMessages = await Message.count({ - where: { ticketId: ticket.id, read: false } - }); - } } export default Ticket; diff --git a/backend/src/services/ContactServices/CreateOrUpdateContactService.ts b/backend/src/services/ContactServices/CreateOrUpdateContactService.ts new file mode 100644 index 0000000..10a25af --- /dev/null +++ b/backend/src/services/ContactServices/CreateOrUpdateContactService.ts @@ -0,0 +1,59 @@ +import { getIO } from "../../libs/socket"; +import Contact from "../../models/Contact"; + +interface ExtraInfo { + name: string; + value: string; +} + +interface Request { + name: string; + number: string; + isGroup: boolean; + email?: string; + profilePicUrl?: string; + extraInfo?: ExtraInfo[]; +} + +const CreateOrUpdateContactService = async ({ + name, + number: rawNumber, + profilePicUrl, + isGroup, + email = "", + extraInfo = [] +}: Request): Promise => { + const number = isGroup ? rawNumber : rawNumber.replace(/[^0-9]/g, ""); + + const io = getIO(); + let contact: Contact | null; + + contact = await Contact.findOne({ where: { number } }); + + if (contact) { + contact.update({ profilePicUrl }); + + io.emit("contact", { + action: "update", + contact + }); + } else { + contact = await Contact.create({ + name, + number, + profilePicUrl, + email, + isGroup, + extraInfo + }); + + io.emit("contact", { + action: "create", + contact + }); + } + + return contact; +}; + +export default CreateOrUpdateContactService; diff --git a/backend/src/services/MessageServices/CreateMessageService.ts b/backend/src/services/MessageServices/CreateMessageService.ts index a98c1c6..857ec5c 100644 --- a/backend/src/services/MessageServices/CreateMessageService.ts +++ b/backend/src/services/MessageServices/CreateMessageService.ts @@ -1,6 +1,6 @@ -import AppError from "../../errors/AppError"; +import { getIO } from "../../libs/socket"; import Message from "../../models/Message"; -import ShowTicketService from "../TicketServices/ShowTicketService"; +import Ticket from "../../models/Ticket"; interface MessageData { id: string; @@ -19,17 +19,16 @@ interface Request { const CreateMessageService = async ({ messageData }: Request): Promise => { - const ticket = await ShowTicketService(messageData.ticketId); - - if (!ticket) { - throw new AppError("ERR_NO_TICKET_FOUND", 404); - } - await Message.upsert(messageData); const message = await Message.findByPk(messageData.id, { include: [ "contact", + { + model: Ticket, + as: "ticket", + include: ["contact"] + }, { model: Message, as: "quotedMsg", @@ -39,9 +38,20 @@ const CreateMessageService = async ({ }); if (!message) { - throw new AppError("ERR_CREATING_MESSAGE", 501); + throw new Error("ERR_CREATING_MESSAGE"); } + const io = getIO(); + io.to(message.ticketId.toString()) + .to(message.ticket.status) + .to("notification") + .emit("appMessage", { + action: "create", + message, + ticket: message.ticket, + contact: message.ticket.contact + }); + return message; }; diff --git a/backend/src/services/TicketServices/FindOrCreateTicketService.ts b/backend/src/services/TicketServices/FindOrCreateTicketService.ts new file mode 100644 index 0000000..b1b2abe --- /dev/null +++ b/backend/src/services/TicketServices/FindOrCreateTicketService.ts @@ -0,0 +1,81 @@ +import { subHours } from "date-fns"; +import { Op } from "sequelize"; +import Contact from "../../models/Contact"; +import Ticket from "../../models/Ticket"; +import ShowTicketService from "./ShowTicketService"; + +const FindOrCreateTicketService = async ( + contact: Contact, + whatsappId: number, + unreadMessages: number, + groupContact?: Contact +): Promise => { + let ticket = await Ticket.findOne({ + where: { + status: { + [Op.or]: ["open", "pending"] + }, + contactId: groupContact ? groupContact.id : contact.id + }, + include: ["contact"] + }); + + if (ticket) { + ticket.update({ unreadMessages }); + } + + if (!ticket && groupContact) { + ticket = await Ticket.findOne({ + where: { + contactId: groupContact.id + }, + order: [["updatedAt", "DESC"]], + include: ["contact"] + }); + + if (ticket) { + await ticket.update({ + status: "pending", + userId: null, + unreadMessages + }); + } + } + + if (!ticket && !groupContact) { + ticket = await Ticket.findOne({ + where: { + updatedAt: { + [Op.between]: [+subHours(new Date(), 2), +new Date()] + }, + contactId: contact.id + }, + order: [["updatedAt", "DESC"]], + include: ["contact"] + }); + + if (ticket) { + await ticket.update({ + status: "pending", + userId: null, + unreadMessages + }); + } + } + + if (!ticket) { + const { id } = await Ticket.create({ + contactId: groupContact ? groupContact.id : contact.id, + status: "pending", + isGroup: !!groupContact, + unreadMessages, + whatsappId + }); + + ticket = await ShowTicketService(id); + } + + return ticket; +}; + +export default FindOrCreateTicketService; diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index fec2177..09105d7 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -1,8 +1,6 @@ import { join } from "path"; import { promisify } from "util"; import { writeFile } from "fs"; -import { Op } from "sequelize"; -import { subHours } from "date-fns"; import * as Sentry from "@sentry/node"; import { @@ -17,10 +15,10 @@ import Ticket from "../../models/Ticket"; import Message from "../../models/Message"; import { getIO } from "../../libs/socket"; -import AppError from "../../errors/AppError"; -import ShowTicketService from "../TicketServices/ShowTicketService"; import CreateMessageService from "../MessageServices/CreateMessageService"; import { logger } from "../../utils/logger"; +import CreateOrUpdateContactService from "../ContactServices/CreateOrUpdateContactService"; +import FindOrCreateTicketService from "../TicketServices/FindOrCreateTicketService"; interface Session extends Client { id?: number; @@ -28,141 +26,48 @@ interface Session extends Client { const writeFileAsync = promisify(writeFile); -const verifyContact = async ( - msgContact: WbotContact, - profilePicUrl: string -): Promise => { - const io = getIO(); +const verifyContact = async (msgContact: WbotContact): Promise => { + const profilePicUrl = await msgContact.getProfilePicUrl(); - let contact = await Contact.findOne({ - where: { number: msgContact.id.user } - }); + const contactData = { + name: msgContact.name || msgContact.pushname || msgContact.id.user, + number: msgContact.id.user, + profilePicUrl, + isGroup: msgContact.isGroup + }; - if (contact) { - await contact.update({ profilePicUrl }); - - io.emit("contact", { - action: "update", - contact - }); - } else { - contact = await Contact.create({ - name: msgContact.name || msgContact.pushname || msgContact.id.user, - number: msgContact.id.user, - profilePicUrl - }); - - io.emit("contact", { - action: "create", - contact - }); - } + const contact = CreateOrUpdateContactService(contactData); return contact; }; -const verifyGroup = async (msgGroupContact: WbotContact) => { - const profilePicUrl = await msgGroupContact.getProfilePicUrl(); +const verifyQuotedMessage = async ( + msg: WbotMessage +): Promise => { + if (!msg.hasQuotedMsg) return null; - let groupContact = await Contact.findOne({ - where: { number: msgGroupContact.id.user } - }); - if (groupContact) { - await groupContact.update({ profilePicUrl }); - } else { - groupContact = await Contact.create({ - name: msgGroupContact.name, - number: msgGroupContact.id.user, - isGroup: msgGroupContact.isGroup, - profilePicUrl - }); - const io = getIO(); - io.emit("contact", { - action: "create", - contact: groupContact - }); - } + const wbotQuotedMsg = await msg.getQuotedMessage(); - return groupContact; -}; - -const verifyTicket = async ( - contact: Contact, - whatsappId: number, - groupContact?: Contact -): Promise => { - let ticket = await Ticket.findOne({ - where: { - status: { - [Op.or]: ["open", "pending"] - }, - contactId: groupContact ? groupContact.id : contact.id - }, - include: ["contact"] + const quotedMsg = await Message.findOne({ + where: { id: wbotQuotedMsg.id.id } }); - if (!ticket && groupContact) { - ticket = await Ticket.findOne({ - where: { - contactId: groupContact.id - }, - order: [["createdAt", "DESC"]], - include: ["contact"] - }); + if (!quotedMsg) return null; - if (ticket) { - await ticket.update({ status: "pending", userId: null }); - } - } - - if (!ticket) { - ticket = await Ticket.findOne({ - where: { - updatedAt: { - [Op.between]: [+subHours(new Date(), 2), +new Date()] - }, - contactId: groupContact ? groupContact.id : contact.id - }, - order: [["updatedAt", "DESC"]], - include: ["contact"] - }); - - if (ticket) { - await ticket.update({ status: "pending", userId: null }); - } - } - - if (!ticket) { - const { id } = await Ticket.create({ - contactId: groupContact ? groupContact.id : contact.id, - status: "pending", - isGroup: !!groupContact, - whatsappId - }); - - ticket = await ShowTicketService(id); - } - - return ticket; + return quotedMsg; }; -const verifyMedia = async ( +const verifyMediaMessage = async ( msg: WbotMessage, ticket: Ticket, contact: Contact ): Promise => { - let quotedMsg: Message | null = null; + const quotedMsg = await verifyQuotedMessage(msg); const media = await msg.downloadMedia(); if (!media) { - throw new AppError("ERR_WAPP_DOWNLOAD_MEDIA"); - } - - if (msg.hasQuotedMsg) { - const wbotQuotedMsg = await msg.getQuotedMessage(); - - quotedMsg = await Message.findByPk(wbotQuotedMsg.id.id); + throw new Error("ERR_WAPP_DOWNLOAD_MEDIA"); } if (!media.filename) { @@ -194,8 +99,8 @@ const verifyMedia = async ( }; const newMessage = await CreateMessageService({ messageData }); - await ticket.update({ lastMessage: msg.body || media.filename }); + return newMessage; }; @@ -204,43 +109,21 @@ const verifyMessage = async ( ticket: Ticket, contact: Contact ) => { - let newMessage: Message | null; - let quotedMsg: Message | null = null; + const quotedMsg = await verifyQuotedMessage(msg); - if (msg.hasQuotedMsg) { - const wbotQuotedMsg = await msg.getQuotedMessage(); + const messageData = { + id: msg.id.id, + ticketId: ticket.id, + contactId: msg.fromMe ? undefined : contact.id, + body: msg.body, + fromMe: msg.fromMe, + mediaType: msg.type, + read: msg.fromMe, + quotedMsgId: quotedMsg?.id + }; - quotedMsg = await Message.findByPk(wbotQuotedMsg.id.id); - } - - if (msg.hasMedia) { - newMessage = await verifyMedia(msg, ticket, contact); - } else { - const messageData = { - id: msg.id.id, - ticketId: ticket.id, - contactId: msg.fromMe ? undefined : contact.id, - body: msg.body, - fromMe: msg.fromMe, - mediaType: msg.type, - read: msg.fromMe, - quotedMsgId: quotedMsg?.id - }; - - newMessage = await CreateMessageService({ messageData }); - await ticket.update({ lastMessage: msg.body }); - } - - const io = getIO(); - io.to(ticket.id.toString()) - .to(ticket.status) - .to("notification") - .emit("appMessage", { - action: "create", - message: newMessage, - ticket, - contact - }); + await CreateMessageService({ messageData }); + await ticket.update({ lastMessage: msg.body }); }; const isValidMsg = (msg: WbotMessage): boolean => { @@ -263,50 +146,66 @@ const handleMessage = async ( msg: WbotMessage, wbot: Session ): Promise => { - if (!isValidMsg(msg)) { - return; - } - - try { - let msgContact: WbotContact; - let groupContact: Contact | undefined; - - if (msg.fromMe) { - msgContact = await wbot.getContactById(msg.to); - - // media messages sent from me from cell phone, first comes with "hasMedia = false" and type = "image/ptt/etc" - // the media itself comes on body of message, as base64 - // if this is the case, return and let this media be handled by media_uploaded event - // it should be improoved to handle the base64 media here in future versions - - if (!msg.hasMedia && msg.type !== "chat" && msg.type !== "vcard") return; - } else { - msgContact = await msg.getContact(); - } - - const chat = await msg.getChat(); - - if (chat.isGroup) { - let msgGroupContact; - - if (msg.fromMe) { - msgGroupContact = await wbot.getContactById(msg.to); - } else { - msgGroupContact = await wbot.getContactById(msg.from); + return new Promise((resolve, reject) => { + (async () => { + if (!isValidMsg(msg)) { + return; } - groupContact = await verifyGroup(msgGroupContact); - } + try { + let msgContact: WbotContact; + let groupContact: Contact | undefined; - const profilePicUrl = await msgContact.getProfilePicUrl(); - const contact = await verifyContact(msgContact, profilePicUrl); - const ticket = await verifyTicket(contact, wbot.id!, groupContact); + if (msg.fromMe) { + // 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" - await verifyMessage(msg, ticket, contact); - } catch (err) { - Sentry.captureException(err); - logger.error(err); - } + if (!msg.hasMedia && msg.type !== "chat" && msg.type !== "vcard") + return; + + msgContact = await wbot.getContactById(msg.to); + } else { + msgContact = await msg.getContact(); + } + + const chat = await msg.getChat(); + + if (chat.isGroup) { + let msgGroupContact; + + if (msg.fromMe) { + msgGroupContact = await wbot.getContactById(msg.to); + } else { + msgGroupContact = await wbot.getContactById(msg.from); + } + + groupContact = await verifyContact(msgGroupContact); + } + + const contact = await verifyContact(msgContact); + const ticket = await FindOrCreateTicketService( + contact, + wbot.id!, + chat.unreadCount, + groupContact + ); + + if (msg.hasMedia) { + await verifyMediaMessage(msg, ticket, contact); + resolve(); + } else { + await verifyMessage(msg, ticket, contact); + resolve(); + } + + await verifyMessage(msg, ticket, contact); + } catch (err) { + Sentry.captureException(err); + logger.error(`Error handling whatsapp message: Err: ${err}`); + reject(err); + } + })(); + }); }; const handleMsgAck = async (msg: WbotMessage, ack: MessageAck) => { @@ -336,7 +235,7 @@ const handleMsgAck = async (msg: WbotMessage, ack: MessageAck) => { }); } catch (err) { Sentry.captureException(err); - logger.log(err); + logger.error(`Error handling message ack. Err: ${err}`); } }; diff --git a/backend/src/services/WbotServices/wbotMonitor.ts b/backend/src/services/WbotServices/wbotMonitor.ts index 0803324..3989fe8 100644 --- a/backend/src/services/WbotServices/wbotMonitor.ts +++ b/backend/src/services/WbotServices/wbotMonitor.ts @@ -19,7 +19,7 @@ const wbotMonitor = async ( try { wbot.on("change_state", async newState => { - logger.info("Monitor session:", sessionName, newState); + logger.info(`Monitor session: ${sessionName}, ${newState}`); try { await whatsapp.update({ status: newState }); } catch (err) { @@ -53,7 +53,7 @@ const wbotMonitor = async ( }); wbot.on("disconnected", async reason => { - logger.info("Disconnected session:", sessionName, reason); + logger.info(`Disconnected session: ${sessionName}, reason: ${reason}`); try { await whatsapp.update({ status: "OPENING", session: "" }); } catch (err) {