diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json index 5f24a5b..9edbbb1 100644 --- a/backend/.eslintrc.json +++ b/backend/.eslintrc.json @@ -1,7 +1,8 @@ { "env": { "es2021": true, - "node": true + "node": true, + "jest": true }, "extends": [ "airbnb-base", diff --git a/backend/.gitignore b/backend/.gitignore index 15b445f..36e4483 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -3,6 +3,7 @@ public/* dist !public/.gitkeep .env +.env.test package-lock.json yarn.lock diff --git a/backend/coverage/lcov-report/AuthServices/RefreshTokenService.ts.html b/backend/coverage/lcov-report/AuthServices/RefreshTokenService.ts.html new file mode 100644 index 0000000..1c5845d --- /dev/null +++ b/backend/coverage/lcov-report/AuthServices/RefreshTokenService.ts.html @@ -0,0 +1,203 @@ + + + + +
++ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { verify } from "jsonwebtoken"; +import AppError from "../../errors/AppError"; +import ShowUserService from "../UserServices/ShowUserService"; +import authConfig from "../../config/auth"; +import { + createAccessToken, + createRefreshToken +} from "../../helpers/CreateTokens"; + +interface RefreshTokenPayload { + id: string; + tokenVersion: number; +} + +interface Response { + newToken: string; + refreshToken: string; +} + +export const RefreshTokenService = async (token: string): Promise<Response> => { + let decoded; + + try { + decoded = verify(token, authConfig.refreshSecret); + } catch (err) { + throw new AppError("ERR_SESSION_EXPIRED", 401); + } + + const { id, tokenVersion } = decoded as RefreshTokenPayload; + + const user = await ShowUserService(id); + + if (user.tokenVersion !== tokenVersion) { + throw new AppError("ERR_SESSION_EXPIRED", 401); + } + + const newToken = createAccessToken(user); + const refreshToken = createRefreshToken(user); + + return { newToken, refreshToken }; +}; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| RefreshTokenService.ts | +
+
+ |
+ 0% | +0/41 | +0% | +0/1 | +0% | +0/1 | +0% | +0/41 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import AppError from "../../errors/AppError"; +import Contact from "../../models/Contact"; + +interface ExtraInfo { + name: string; + value: string; +} + +interface Request { + name: string; + number: string; + email?: string; + profilePicUrl?: string; + extraInfo?: ExtraInfo[]; +} + +const CreateContactService = async ({ + name, + number, + email = "", + extraInfo = [] +}: Request): Promise<Contact> => { + const numberExists = await Contact.findOne({ + where: { number } + }); + + if (numberExists) { + throw new AppError("ERR_DUPLICATED_CONTACT"); + } + + const contact = await Contact.create( + { + name, + number, + email, + extraInfo + }, + { + include: ["extraInfo"] + } + ); + + return contact; +}; + +export default CreateContactService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 | + + + + + + + + + + + + + + + + | import Contact from "../../models/Contact"; +import AppError from "../../errors/AppError"; + +const DeleteContactService = async (id: string): Promise<void> => { + const contact = await Contact.findOne({ + where: { id } + }); + + if (!contact) { + throw new AppError("ERR_NO_CONTACT_FOUND", 404); + } + + await contact.destroy(); +}; + +export default DeleteContactService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { Sequelize, Op } from "sequelize"; +import Contact from "../../models/Contact"; + +interface Request { + searchParam?: string; + pageNumber?: string; +} + +interface Response { + contacts: Contact[]; + count: number; + hasMore: boolean; +} + +const ListContactsService = async ({ + searchParam = "", + pageNumber = "1" +}: Request): Promise<Response> => { + const whereCondition = { + [Op.or]: [ + { + name: Sequelize.where( + Sequelize.fn("LOWER", Sequelize.col("name")), + "LIKE", + `%${searchParam.toLowerCase().trim()}%` + ) + }, + { number: { [Op.like]: `%${searchParam.toLowerCase().trim()}%` } } + ] + }; + const limit = 20; + const offset = limit * (+pageNumber - 1); + + const { count, rows: contacts } = await Contact.findAndCountAll({ + where: whereCondition, + limit, + offset, + order: [["name", "ASC"]] + }); + + const hasMore = count > offset + contacts.length; + + return { + contacts, + count, + hasMore + }; +}; + +export default ListContactsService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 | + + + + + + + + + + + + + + | import Contact from "../../models/Contact"; +import AppError from "../../errors/AppError"; + +const ShowContactService = async (id: string | number): Promise<Contact> => { + const contact = await Contact.findByPk(id, { include: ["extraInfo"] }); + + if (!contact) { + throw new AppError("ERR_NO_CONTACT_FOUND", 404); + } + + return contact; +}; + +export default ShowContactService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import AppError from "../../errors/AppError"; +import Contact from "../../models/Contact"; +import ContactCustomField from "../../models/ContactCustomField"; + +interface ExtraInfo { + id?: number; + name: string; + value: string; +} +interface ContactData { + email?: string; + number?: string; + name?: string; + extraInfo?: ExtraInfo[]; +} + +interface Request { + contactData: ContactData; + contactId: string; +} + +const UpdateContactService = async ({ + contactData, + contactId +}: Request): Promise<Contact> => { + const { email, name, number, extraInfo } = contactData; + + const contact = await Contact.findOne({ + where: { id: contactId }, + attributes: ["id", "name", "number", "email", "profilePicUrl"], + include: ["extraInfo"] + }); + + if (!contact) { + throw new AppError("ERR_NO_CONTACT_FOUND", 404); + } + + if (extraInfo) { + await Promise.all( + extraInfo.map(async info => { + await ContactCustomField.upsert({ ...info, contactId: contact.id }); + }) + ); + + await Promise.all( + contact.extraInfo.map(async oldInfo => { + const stillExists = extraInfo.findIndex(info => info.id === oldInfo.id); + + if (stillExists === -1) { + await ContactCustomField.destroy({ where: { id: oldInfo.id } }); + } + }) + ); + } + + await contact.update({ + name, + number, + email + }); + + await contact.reload({ + attributes: ["id", "name", "number", "email", "profilePicUrl"], + include: ["extraInfo"] + }); + + return contact; +}; + +export default UpdateContactService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| CreateContactService.ts | +
+
+ |
+ 0% | +0/46 | +0% | +0/1 | +0% | +0/1 | +0% | +0/46 | +
| DeleteContactService.ts | +
+
+ |
+ 0% | +0/16 | +0% | +0/1 | +0% | +0/1 | +0% | +0/16 | +
| ListContactsService.ts | +
+
+ |
+ 0% | +0/50 | +0% | +0/1 | +0% | +0/1 | +0% | +0/50 | +
| ShowContactService.ts | +
+
+ |
+ 0% | +0/14 | +0% | +0/1 | +0% | +0/1 | +0% | +0/14 | +
| UpdateContactService.ts | +
+
+ |
+ 0% | +0/70 | +0% | +0/1 | +0% | +0/1 | +0% | +0/70 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import AppError from "../../errors/AppError"; +import Message from "../../models/Message"; +import ShowTicketService from "../TicketServices/ShowTicketService"; + +interface MessageData { + id: string; + ticketId: number; + body: string; + contactId?: number; + fromMe?: boolean; + read?: boolean; + mediaType?: string; + mediaUrl?: string; +} +interface Request { + messageData: MessageData; +} + +const CreateMessageService = async ({ + messageData +}: Request): Promise<Message> => { + 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"] + }); + + if (!message) { + throw new AppError("ERR_CREATING_MESSAGE", 501); + } + + return message; +}; + +export default CreateMessageService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { where, fn, col } from "sequelize"; +import AppError from "../../errors/AppError"; +import Message from "../../models/Message"; +import Ticket from "../../models/Ticket"; +import ShowTicketService from "../TicketServices/ShowTicketService"; + +interface Request { + ticketId: string; + searchParam?: string; + pageNumber?: string; +} + +interface Response { + messages: Message[]; + ticket: Ticket; + count: number; + hasMore: boolean; +} + +const ListMessagesService = async ({ + searchParam = "", + pageNumber = "1", + ticketId +}: Request): Promise<Response> => { + const ticket = await ShowTicketService(ticketId); + + if (!ticket) { + throw new AppError("ERR_NO_TICKET_FOUND", 404); + } + + const whereCondition = { + body: where( + fn("LOWER", col("body")), + "LIKE", + `%${searchParam.toLowerCase()}%` + ), + ticketId + }; + + // await setMessagesAsRead(ticket); + const limit = 20; + const offset = limit * (+pageNumber - 1); + + const { count, rows: messages } = await Message.findAndCountAll({ + where: whereCondition, + limit, + include: ["contact"], + offset, + order: [["createdAt", "DESC"]] + }); + + const hasMore = count > offset + messages.length; + + return { + messages: messages.reverse(), + ticket, + count, + hasMore + }; +}; + +export default ListMessagesService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| CreateMessageService.ts | +
+
+ |
+ 0% | +0/41 | +0% | +0/1 | +0% | +0/1 | +0% | +0/41 | +
| ListMessagesService.ts | +
+
+ |
+ 0% | +0/62 | +0% | +0/1 | +0% | +0/1 | +0% | +0/62 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 | + + + + + + + + + | import Setting from "../../models/Setting"; + +const ListSettingsService = async (): Promise<Setting[] | undefined> => { + const settings = await Setting.findAll(); + + return settings; +}; + +export default ListSettingsService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 | + + + + + + + + + + + + + + + + + + + + + + + + + + | import AppError from "../../errors/AppError"; +import Setting from "../../models/Setting"; + +interface Request { + key: string; + value: string; +} + +const UpdateSettingService = async ({ + key, + value +}: Request): Promise<Setting | undefined> => { + const setting = await Setting.findOne({ + where: { key } + }); + + if (!setting) { + throw new AppError("ERR_NO_SETTING_FOUND", 404); + } + + await setting.update({ value }); + + return setting; +}; + +export default UpdateSettingService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| ListSettingsService.ts | +
+
+ |
+ 0% | +0/9 | +0% | +0/1 | +0% | +0/1 | +0% | +0/9 | +
| UpdateSettingService.ts | +
+
+ |
+ 0% | +0/26 | +0% | +0/1 | +0% | +0/1 | +0% | +0/26 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import AppError from "../../errors/AppError"; +import CheckContactOpenTickets from "../../helpers/CheckContactOpenTickets"; +import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; +import Ticket from "../../models/Ticket"; +import ShowContactService from "../ContactServices/ShowContactService"; + +interface Request { + contactId: number; + status: string; + userId: number; +} + +const CreateTicketService = async ({ + contactId, + status, + userId +}: Request): Promise<Ticket> => { + const defaultWhatsapp = await GetDefaultWhatsApp(); + + await CheckContactOpenTickets(contactId); + + const { isGroup } = await ShowContactService(contactId); + + const { id }: Ticket = await defaultWhatsapp.$create("ticket", { + contactId, + status, + isGroup, + userId + }); + + const ticket = await Ticket.findByPk(id, { include: ["contact"] }); + + if (!ticket) { + throw new AppError("ERR_CREATING_TICKET"); + } + + return ticket; +}; + +export default CreateTicketService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 | + + + + + + + + + + + + + + + + + + | import Ticket from "../../models/Ticket"; +import AppError from "../../errors/AppError"; + +const DeleteTicketService = async (id: string): Promise<Ticket> => { + const ticket = await Ticket.findOne({ + where: { id } + }); + + if (!ticket) { + throw new AppError("ERR_NO_TICKET_FOUND", 404); + } + + await ticket.destroy(); + + return ticket; +}; + +export default DeleteTicketService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { Op, fn, where, col, Filterable, Includeable } from "sequelize"; +import { startOfDay, endOfDay, parseISO } from "date-fns"; + +import Ticket from "../../models/Ticket"; +import Contact from "../../models/Contact"; +import Message from "../../models/Message"; + +interface Request { + searchParam?: string; + pageNumber?: string; + status?: string; + date?: string; + showAll?: string; + userId: string; + withUnreadMessages?: string; +} + +interface Response { + tickets: Ticket[]; + count: number; + hasMore: boolean; +} + +const ListTicketsService = async ({ + searchParam = "", + pageNumber = "1", + status, + date, + showAll, + userId, + withUnreadMessages +}: Request): Promise<Response> => { + let whereCondition: Filterable["where"] = { + [Op.or]: [{ userId }, { status: "pending" }] + }; + let includeCondition: Includeable[]; + + includeCondition = [ + { + model: Contact, + as: "contact", + attributes: ["id", "name", "number", "profilePicUrl"] + } + ]; + + if (showAll === "true") { + whereCondition = {}; + } + + if (status) { + whereCondition = { + ...whereCondition, + status + }; + } + + if (searchParam) { + const sanitizedSearchParam = searchParam.toLocaleLowerCase().trim(); + + includeCondition = [ + ...includeCondition, + { + model: Message, + as: "messages", + attributes: ["id", "body"], + where: { + body: where( + fn("LOWER", col("body")), + "LIKE", + `%${sanitizedSearchParam}%` + ) + }, + required: false, + duplicating: false + } + ]; + + whereCondition = { + [Op.or]: [ + { + "$contact.name$": where( + fn("LOWER", col("name")), + "LIKE", + `%${sanitizedSearchParam}%` + ) + }, + { "$contact.number$": { [Op.like]: `%${sanitizedSearchParam}%` } }, + { + "$message.body$": where( + fn("LOWER", col("body")), + "LIKE", + `%${sanitizedSearchParam}%` + ) + } + ] + }; + } + + if (date) { + whereCondition = { + ...whereCondition, + createdAt: { + [Op.between]: [+startOfDay(parseISO(date)), +endOfDay(parseISO(date))] + } + }; + } + + if (withUnreadMessages === "true") { + includeCondition = [ + ...includeCondition, + { + model: Message, + as: "messages", + attributes: [], + where: { + read: false, + fromMe: false + } + } + ]; + } + + const limit = 20; + const offset = limit * (+pageNumber - 1); + + const { count, rows: tickets } = await Ticket.findAndCountAll({ + where: whereCondition, + include: includeCondition, + distinct: true, + limit, + offset, + order: [["updatedAt", "DESC"]] + }); + + const hasMore = count > offset + tickets.length; + + return { + tickets, + count, + hasMore + }; +}; + +export default ListTicketsService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import Ticket from "../../models/Ticket"; +import AppError from "../../errors/AppError"; +import Contact from "../../models/Contact"; +import User from "../../models/User"; + +const ShowTicketService = async (id: string | number): Promise<Ticket> => { + const ticket = await Ticket.findByPk(id, { + include: [ + { + model: Contact, + as: "contact", + attributes: ["id", "name", "number", "profilePicUrl"], + include: ["extraInfo"] + }, + { + model: User, + as: "user", + attributes: ["id", "name"] + } + ] + }); + + if (!ticket) { + throw new AppError("ERR_NO_TICKET_FOUND", 404); + } + + return ticket; +}; + +export default ShowTicketService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import AppError from "../../errors/AppError"; +import CheckContactOpenTickets from "../../helpers/CheckContactOpenTickets"; +import SetTicketMessagesAsRead from "../../helpers/SetTicketMessagesAsRead"; +import Contact from "../../models/Contact"; +import Ticket from "../../models/Ticket"; +import User from "../../models/User"; + +interface TicketData { + status?: string; + userId?: number; +} + +interface Request { + ticketData: TicketData; + ticketId: string; +} + +interface Response { + ticket: Ticket; + ticketUser: User | null; + oldStatus: string; +} + +const UpdateTicketService = async ({ + ticketData, + ticketId +}: Request): Promise<Response> => { + const { status, userId } = ticketData; + + const ticket = await Ticket.findOne({ + where: { id: ticketId }, + include: [ + { + model: Contact, + as: "contact", + attributes: ["id", "name", "number", "profilePicUrl"] + } + ] + }); + + if (!ticket) { + throw new AppError("ERR_NO_TICKET_FOUND", 404); + } + + await SetTicketMessagesAsRead(ticket); + + const oldStatus = ticket.status; + + if (oldStatus === "closed") { + await CheckContactOpenTickets(ticket.contact.id); + } + + await ticket.update({ + status, + userId + }); + const ticketUser = await ticket.$get("user", { attributes: ["id", "name"] }); + + return { ticket, oldStatus, ticketUser }; +}; + +export default UpdateTicketService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| CreateTicketService.ts | +
+
+ |
+ 0% | +0/40 | +0% | +0/1 | +0% | +0/1 | +0% | +0/40 | +
| DeleteTicketService.ts | +
+
+ |
+ 0% | +0/18 | +0% | +0/1 | +0% | +0/1 | +0% | +0/18 | +
| ListTicketsService.ts | +
+
+ |
+ 0% | +0/144 | +0% | +0/1 | +0% | +0/1 | +0% | +0/144 | +
| ShowTicketService.ts | +
+
+ |
+ 0% | +0/30 | +0% | +0/1 | +0% | +0/1 | +0% | +0/30 | +
| UpdateTicketService.ts | +
+
+ |
+ 0% | +0/62 | +0% | +0/1 | +0% | +0/1 | +0% | +0/62 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import User from "../../models/User"; +import AppError from "../../errors/AppError"; +import { + createAccessToken, + createRefreshToken +} from "../../helpers/CreateTokens"; + +interface Request { + email: string; + password: string; +} + +interface Response { + user: User; + token: string; + refreshToken: string; +} + +const AuthUserService = async ({ + email, + password +}: Request): Promise<Response> => { + const user = await User.findOne({ + where: { email } + }); + + if (!user) { + throw new AppError("ERR_INVALID_CREDENTIALS", 401); + } + + if (!(await user.checkPassword(password))) { + throw new AppError("ERR_INVALID_CREDENTIALS", 401); + } + + const token = createAccessToken(user); + const refreshToken = createRefreshToken(user); + + return { + user, + token, + refreshToken + }; +}; + +export default AuthUserService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import * as Yup from "yup"; + +import AppError from "../../errors/AppError"; +import User from "../../models/User"; + +interface Request { + email: string; + password: string; + name: string; + profile?: string; +} + +interface Response { + email: string; + name: string; + id: number; + profile: string; +} + +const CreateUserService = async ({ + email, + password, + name, + profile = "admin" +}: Request): Promise<Response> => { + const schema = Yup.object().shape({ + name: Yup.string().required().min(2), + email: Yup.string() + .email() + .required() + .test( + "Check-email", + "An user with this email already exists.", + async value => { + if (value) { + const emailExists = await User.findOne({ + where: { email: value } + }); + return !emailExists; + } + return false; + } + ), + password: Yup.string().required().min(5) + }); + + try { + await schema.validate({ email, password, name }); + } catch (err) { + throw new AppError(err.message); + } + + const user = await User.create({ + email, + password, + name, + profile + }); + + const serializedUser = { + id: user.id, + name: user.name, + email: user.email, + profile: user.profile + }; + + return serializedUser; +}; + +export default CreateUserService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 | + + + + + + + + + + + + + + + + + + + + + + + + + + | import User from "../../models/User"; +import AppError from "../../errors/AppError"; +import Ticket from "../../models/Ticket"; +import UpdateDeletedUserOpenTicketsStatus from "../../helpers/UpdateDeletedUserOpenTicketsStatus"; + +const DeleteUserService = async (id: string): Promise<void> => { + const user = await User.findOne({ + where: { id } + }); + + if (!user) { + throw new AppError("ERR_NO_USER_FOUND", 404); + } + + const userOpenTickets: Ticket[] = await user.$get("tickets", { + where: { status: "open" } + }); + + if (userOpenTickets.length > 0) { + UpdateDeletedUserOpenTicketsStatus(userOpenTickets); + } + + await user.destroy(); +}; + +export default DeleteUserService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { Sequelize, Op } from "sequelize"; +import User from "../../models/User"; + +interface Request { + searchParam?: string; + pageNumber?: string; +} + +interface Response { + users: User[]; + count: number; + hasMore: boolean; +} + +const ListUsersService = async ({ + searchParam = "", + pageNumber = "1" +}: Request): Promise<Response> => { + const whereCondition = { + [Op.or]: [ + { + name: Sequelize.where( + Sequelize.fn("LOWER", Sequelize.col("name")), + "LIKE", + `%${searchParam.toLowerCase()}%` + ) + }, + { email: { [Op.like]: `%${searchParam.toLowerCase()}%` } } + ] + }; + const limit = 20; + const offset = limit * (+pageNumber - 1); + + const { count, rows: users } = await User.findAndCountAll({ + where: whereCondition, + attributes: ["name", "id", "email", "profile"], + limit, + offset, + order: [["createdAt", "DESC"]] + }); + + const hasMore = count > offset + users.length; + + return { + users, + count, + hasMore + }; +}; + +export default ListUsersService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 | + + + + + + + + + + + + + + + + | import User from "../../models/User"; +import AppError from "../../errors/AppError"; + +const ShowUserService = async (id: string | number): Promise<User> => { + const user = await User.findByPk(id, { + attributes: ["name", "id", "email", "profile", "tokenVersion"] + }); + + if (!user) { + throw new AppError("ERR_NO_USER_FOUND", 404); + } + + return user; +}; + +export default ShowUserService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import * as Yup from "yup"; + +import AppError from "../../errors/AppError"; +import User from "../../models/User"; + +interface UserData { + email?: string; + password?: string; + name?: string; + profile?: string; +} + +interface Request { + userData: UserData; + userId: string; +} + +interface Response { + id: number; + name: string; + email: string; + profile: string; +} + +const UpdateUserService = async ({ + userData, + userId +}: Request): Promise<Response | undefined> => { + const user = await User.findOne({ + where: { id: userId }, + attributes: ["name", "id", "email", "profile"] + }); + + if (!user) { + throw new AppError("ERR_NO_USER_FOUND", 404); + } + + const schema = Yup.object().shape({ + name: Yup.string().min(2), + email: Yup.string().email(), + password: Yup.string() + }); + + const { email, password, name } = userData; + + try { + await schema.validate({ email, password, name }); + } catch (err) { + throw new AppError(err.message); + } + + await user.update({ + email, + password, + name + }); + + const serializedUser = { + id: user.id, + name: user.name, + email: user.email, + profile: user.profile + }; + + return serializedUser; +}; + +export default UpdateUserService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| AuthUserSerice.ts | +
+
+ |
+ 0% | +0/45 | +0% | +0/1 | +0% | +0/1 | +0% | +0/45 | +
| CreateUserService.ts | +
+
+ |
+ 0% | +0/70 | +0% | +0/1 | +0% | +0/1 | +0% | +0/70 | +
| DeleteUserService.ts | +
+
+ |
+ 0% | +0/26 | +0% | +0/1 | +0% | +0/1 | +0% | +0/26 | +
| ListUsersService.ts | +
+
+ |
+ 0% | +0/51 | +0% | +0/1 | +0% | +0/1 | +0% | +0/51 | +
| ShowUserService.ts | +
+
+ |
+ 0% | +0/16 | +0% | +0/1 | +0% | +0/1 | +0% | +0/16 | +
| UpdateUserService.ts | +
+
+ |
+ 0% | +0/68 | +0% | +0/1 | +0% | +0/1 | +0% | +0/68 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 | + + + + + + + + + + + + + + + + + + + + + + + + | import AppError from "../../errors/AppError"; +import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; +import { getWbot } from "../../libs/wbot"; + +const CheckIsValidContact = async (number: string): Promise<void> => { + const defaultWhatsapp = await GetDefaultWhatsApp(); + + const wbot = getWbot(defaultWhatsapp.id); + + try { + const isValidNumber = await wbot.isRegisteredUser(`${number}@c.us`); + if (!isValidNumber) { + throw new AppError("invalidNumber"); + } + } catch (err) { + console.log(err); + if (err.message === "invalidNumber") { + throw new AppError("ERR_WAPP_INVALID_CONTACT"); + } + throw new AppError("ERR_WAPP_CHECK_CONTACT"); + } +}; + +export default CheckIsValidContact; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import AppError from "../../errors/AppError"; +import GetWbotMessage from "../../helpers/GetWbotMessage"; +import Message from "../../models/Message"; +import Ticket from "../../models/Ticket"; + +const DeleteWhatsAppMessage = async (messageId: string): Promise<Message> => { + const message = await Message.findByPk(messageId, { + include: [ + { + model: Ticket, + as: "ticket", + include: ["contact"] + } + ] + }); + + if (!message) { + throw new AppError("No message found with this ID."); + } + + const { ticket } = message; + + const messageToDelete = await GetWbotMessage(ticket, messageId); + + try { + await messageToDelete.delete(true); + } catch (err) { + throw new AppError("ERR_DELETE_WAPP_MSG"); + } + + await message.update({ isDeleted: true }); + + return message; +}; + +export default DeleteWhatsAppMessage; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 | + + + + + + + + + + + + + + | import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; +import { getWbot } from "../../libs/wbot"; + +const GetProfilePicUrl = async (number: string): Promise<string> => { + const defaultWhatsapp = await GetDefaultWhatsApp(); + + const wbot = getWbot(defaultWhatsapp.id); + + const profilePicUrl = await wbot.getProfilePicUrl(`${number}@c.us`); + + return profilePicUrl; +}; + +export default GetProfilePicUrl; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; +import { getWbot } from "../../libs/wbot"; +import Contact from "../../models/Contact"; + +const ImportContactsService = async (): Promise<void> => { + const defaultWhatsapp = await GetDefaultWhatsApp(); + + const wbot = getWbot(defaultWhatsapp.id); + + let phoneContacts; + + try { + phoneContacts = await wbot.getContacts(); + } catch (err) { + console.log( + "Could not get whatsapp contacts from phone. Check connection page.", + err + ); + } + + if (phoneContacts) { + await Promise.all( + phoneContacts.map(async ({ number, name }) => { + if (!number) { + return null; + } + if (!name) { + name = number; + } + + const numberExists = await Contact.findOne({ + where: { number } + }); + + if (numberExists) return null; + + return Contact.create({ number, name }); + }) + ); + } +}; + +export default ImportContactsService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import fs from "fs"; +import { MessageMedia, Message as WbotMessage } from "whatsapp-web.js"; +import AppError from "../../errors/AppError"; +import GetTicketWbot from "../../helpers/GetTicketWbot"; +import Ticket from "../../models/Ticket"; + +interface Request { + media: Express.Multer.File; + ticket: Ticket; +} + +const SendWhatsAppMedia = async ({ + media, + ticket +}: Request): Promise<WbotMessage> => { + try { + const wbot = await GetTicketWbot(ticket); + + const newMedia = MessageMedia.fromFilePath(media.path); + + const sentMessage = await wbot.sendMessage( + `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`, + newMedia, + { sendAudioAsVoice: true } + ); + + await ticket.update({ lastMessage: media.filename }); + + fs.unlinkSync(media.path); + + return sentMessage; + } catch (err) { + console.log(err); + throw new AppError("ERR_SENDING_WAPP_MSG"); + } +}; + +export default SendWhatsAppMedia; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import { Message as WbotMessage } from "whatsapp-web.js"; +import AppError from "../../errors/AppError"; +import GetTicketWbot from "../../helpers/GetTicketWbot"; +import Ticket from "../../models/Ticket"; + +interface Request { + body: string; + ticket: Ticket; +} + +const SendWhatsAppMessage = async ({ + body, + ticket +}: Request): Promise<WbotMessage> => { + try { + const wbot = await GetTicketWbot(ticket); + + const sentMessage = await wbot.sendMessage( + `${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`, + body + ); + + await ticket.update({ lastMessage: body }); + return sentMessage; + } catch (err) { + console.log(err); + throw new AppError("ERR_SENDING_WAPP_MSG"); + } +}; + +export default SendWhatsAppMessage; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 | + + + + + + + + + + + + + + + + + + | import { initWbot } from "../../libs/wbot"; +import Whatsapp from "../../models/Whatsapp"; +import wbotMessageListener from "./wbotMessageListener"; +import wbotMonitor from "./wbotMonitor"; + +export const StartWhatsAppSessions = async (): Promise<void> => { + const whatsapps = await Whatsapp.findAll(); + if (whatsapps.length > 0) { + whatsapps.forEach(whatsapp => { + initWbot(whatsapp) + .then(() => { + wbotMessageListener(whatsapp); + wbotMonitor(whatsapp); + }) + .catch(err => console.log(err)); + }); + } +}; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| CheckIsValidContact.ts | +
+
+ |
+ 0% | +0/24 | +0% | +0/1 | +0% | +0/1 | +0% | +0/24 | +
| DeleteWhatsAppMessage.ts | +
+
+ |
+ 0% | +0/36 | +0% | +0/1 | +0% | +0/1 | +0% | +0/36 | +
| GetProfilePicUrl.ts | +
+
+ |
+ 0% | +0/14 | +0% | +0/1 | +0% | +0/1 | +0% | +0/14 | +
| ImportContactsService.ts | +
+
+ |
+ 0% | +0/43 | +0% | +0/1 | +0% | +0/1 | +0% | +0/43 | +
| SendWhatsAppMedia.ts | +
+
+ |
+ 0% | +0/38 | +0% | +0/1 | +0% | +0/1 | +0% | +0/38 | +
| SendWhatsAppMessage.ts | +
+
+ |
+ 0% | +0/31 | +0% | +0/1 | +0% | +0/1 | +0% | +0/31 | +
| StartWhatsAppSessions.ts | +
+
+ |
+ 0% | +0/18 | +0% | +0/1 | +0% | +0/1 | +0% | +0/18 | +
| wbotMessageListener.ts | +
+
+ |
+ 0% | +0/333 | +0% | +0/1 | +0% | +0/1 | +0% | +0/333 | +
| wbotMonitor.ts | +
+
+ |
+ 0% | +0/87 | +0% | +0/1 | +0% | +0/1 | +0% | +0/87 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | 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 { + Contact as WbotContact, + Message as WbotMessage +} from "whatsapp-web.js"; + +import Contact from "../../models/Contact"; +import Ticket from "../../models/Ticket"; +import Message from "../../models/Message"; +import Whatsapp from "../../models/Whatsapp"; + +import { getIO } from "../../libs/socket"; +import { getWbot } from "../../libs/wbot"; +import AppError from "../../errors/AppError"; +import ShowTicketService from "../TicketServices/ShowTicketService"; +import CreateMessageService from "../MessageServices/CreateMessageService"; + +const writeFileAsync = promisify(writeFile); + +const verifyContact = async ( + msgContact: WbotContact, + profilePicUrl: string +): Promise<Contact> => { + const io = getIO(); + + let contact = await Contact.findOne({ + where: { number: msgContact.id.user } + }); + + 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 + }); + } + + return contact; +}; + +const verifyGroup = async (msgGroupContact: WbotContact) => { + const profilePicUrl = await msgGroupContact.getProfilePicUrl(); + + 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 + }); + } + + return groupContact; +}; + +const verifyTicket = async ( + contact: Contact, + whatsappId: number, + groupContact?: Contact +): Promise<Ticket> => { + let ticket = await Ticket.findOne({ + where: { + status: { + [Op.or]: ["open", "pending"] + }, + contactId: groupContact ? groupContact.id : contact.id + }, + include: ["contact"] + }); + + if (!ticket && groupContact) { + ticket = await Ticket.findOne({ + where: { + contactId: groupContact.id + }, + order: [["createdAt", "DESC"]], + include: ["contact"] + }); + + if (ticket) { + await ticket.update({ status: "pending", userId: null }); + } + } + + if (!ticket) { + ticket = await Ticket.findOne({ + where: { + createdAt: { + [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; +}; + +const handlMedia = async ( + msg: WbotMessage, + ticket: Ticket, + contact: Contact +): Promise<Message> => { + const media = await msg.downloadMedia(); + + if (!media) { + throw new AppError("ERR_WAPP_DOWNLOAD_MEDIA"); + } + + if (!media.filename) { + const ext = media.mimetype.split("/")[1].split(";")[0]; + media.filename = `${new Date().getTime()}.${ext}`; + } + + try { + await writeFileAsync( + join(__dirname, "..", "..", "..", "public", media.filename), + media.data, + "base64" + ); + } catch (err) { + console.log(err); + } + + const messageData = { + id: msg.id.id, + ticketId: ticket.id, + contactId: msg.fromMe ? undefined : contact.id, + body: msg.body || media.filename, + fromMe: msg.fromMe, + read: msg.fromMe, + mediaUrl: media.filename, + mediaType: media.mimetype.split("/")[0] + }; + + const newMessage = await CreateMessageService({ messageData }); + + await ticket.update({ lastMessage: msg.body || media.filename }); + return newMessage; +}; + +const handleMessage = async ( + msg: WbotMessage, + ticket: Ticket, + contact: Contact +) => { + let newMessage: Message | null; + + if (msg.hasMedia) { + newMessage = await handlMedia(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 + }; + + 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 + }); +}; + +const isValidMsg = (msg: WbotMessage): boolean => { + if (msg.from === "status@broadcast") return false; + if ( + msg.type === "chat" || + msg.type === "audio" || + msg.type === "ptt" || + msg.type === "video" || + msg.type === "image" || + msg.type === "document" || + msg.type === "vcard" || + msg.type === "sticker" + ) + return true; + return false; +}; + +const wbotMessageListener = (whatsapp: Whatsapp): void => { + const whatsappId = whatsapp.id; + const wbot = getWbot(whatsappId); + const io = getIO(); + + wbot.on("message_create", async msg => { + // console.log(msg); + if (!isValidMsg(msg)) { + return; + } + + try { + let msgContact: WbotContact; + let groupContact: Contact | undefined; + + if (msg.fromMe) { + msgContact = await wbot.getContactById(msg.to); + + // return if it's a media message, it will be handled by media_uploaded event + + 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); + } + + groupContact = await verifyGroup(msgGroupContact); + } + + const profilePicUrl = await msgContact.getProfilePicUrl(); + const contact = await verifyContact(msgContact, profilePicUrl); + const ticket = await verifyTicket(contact, whatsappId, groupContact); + + await handleMessage(msg, ticket, contact); + } catch (err) { + Sentry.captureException(err); + console.log(err); + } + }); + + wbot.on("media_uploaded", async msg => { + try { + let groupContact: Contact | undefined; + const msgContact = await wbot.getContactById(msg.to); + if (msg.author) { + const msgGroupContact = await wbot.getContactById(msg.from); + groupContact = await verifyGroup(msgGroupContact); + } + + const profilePicUrl = await msgContact.getProfilePicUrl(); + const contact = await verifyContact(msgContact, profilePicUrl); + const ticket = await verifyTicket(contact, whatsappId, groupContact); + + await handleMessage(msg, ticket, contact); + } catch (err) { + Sentry.captureException(err); + console.log(err); + } + }); + + wbot.on("message_ack", async (msg, ack) => { + await new Promise(r => setTimeout(r, 500)); + + try { + const messageToUpdate = await Message.findByPk(msg.id.id, { + include: ["contact"] + }); + if (!messageToUpdate) { + return; + } + await messageToUpdate.update({ ack }); + + io.to(messageToUpdate.ticketId.toString()).emit("appMessage", { + action: "update", + message: messageToUpdate + }); + } catch (err) { + Sentry.captureException(err); + console.log(err); + } + }); +}; + +export default wbotMessageListener; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import * as Sentry from "@sentry/node"; + +import wbotMessageListener from "./wbotMessageListener"; + +import { getIO } from "../../libs/socket"; +import { getWbot, initWbot } from "../../libs/wbot"; +import Whatsapp from "../../models/Whatsapp"; + +const wbotMonitor = (whatsapp: Whatsapp): void => { + const io = getIO(); + const sessionName = whatsapp.name; + const wbot = getWbot(whatsapp.id); + + try { + wbot.on("change_state", async newState => { + console.log("Monitor session:", sessionName, newState); + try { + await whatsapp.update({ status: newState }); + } catch (err) { + Sentry.captureException(err); + console.log(err); + } + + io.emit("whatsappSession", { + action: "update", + session: whatsapp + }); + }); + + wbot.on("change_battery", async batteryInfo => { + const { battery, plugged } = batteryInfo; + console.log( + `Battery session: ${sessionName} ${battery}% - Charging? ${plugged}` + ); + + try { + await whatsapp.update({ battery, plugged }); + } catch (err) { + Sentry.captureException(err); + console.log(err); + } + + io.emit("whatsappSession", { + action: "update", + session: whatsapp + }); + }); + + wbot.on("disconnected", async reason => { + console.log("Disconnected session:", sessionName, reason); + try { + await whatsapp.update({ status: "disconnected" }); + } catch (err) { + Sentry.captureException(err); + console.log(err); + } + + io.emit("whatsappSession", { + action: "update", + session: whatsapp + }); + + setTimeout( + () => + initWbot(whatsapp) + .then(() => { + wbotMessageListener(whatsapp); + wbotMonitor(whatsapp); + }) + .catch(err => { + Sentry.captureException(err); + console.log(err); + }), + 2000 + ); + }); + + // setInterval(() => { + // wbot.resetState(); + // }, 20000); + } catch (err) { + Sentry.captureException(err); + console.log(err); + } +}; + +export default wbotMonitor; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import * as Yup from "yup"; + +import AppError from "../../errors/AppError"; +import Whatsapp from "../../models/Whatsapp"; + +interface Request { + name: string; + status?: string; + isDefault?: boolean; +} + +const CreateWhatsAppService = async ({ + name, + status = "INITIALIZING", + isDefault = false +}: Request): Promise<Whatsapp> => { + const schema = Yup.object().shape({ + name: Yup.string() + .required() + .min(2) + .test( + "Check-name", + "This whatsapp name is already used.", + async value => { + if (value) { + const whatsappFound = await Whatsapp.findOne({ + where: { name: value } + }); + return !whatsappFound; + } + return true; + } + ), + isDefault: Yup.boolean() + .required() + .test( + "Check-default", + "Only one default whatsapp is permitted", + async value => { + if (value === true) { + const whatsappFound = await Whatsapp.findOne({ + where: { isDefault: true } + }); + return !whatsappFound; + } + return true; + } + ) + }); + + try { + await schema.validate({ name, status, isDefault }); + } catch (err) { + throw new AppError(err.message); + } + + const whatsapp = await Whatsapp.create({ + name, + status, + isDefault + }); + + return whatsapp; +}; + +export default CreateWhatsAppService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 | + + + + + + + + + + + + + + + + | import Whatsapp from "../../models/Whatsapp"; +import AppError from "../../errors/AppError"; + +const DeleteWhatsApprService = async (id: string): Promise<void> => { + const whatsapp = await Whatsapp.findOne({ + where: { id } + }); + + if (!whatsapp) { + throw new AppError("ERR_NO_WAPP_FOUND", 404); + } + + await whatsapp.destroy(); +}; + +export default DeleteWhatsApprService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 | + + + + + + + + + | import Whatsapp from "../../models/Whatsapp"; + +const ListWhatsAppsService = async (): Promise<Whatsapp[]> => { + const whatsapps = await Whatsapp.findAll(); + + return whatsapps; +}; + +export default ListWhatsAppsService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 | + + + + + + + + + + + + + + + + | import Whatsapp from "../../models/Whatsapp"; +import AppError from "../../errors/AppError"; + +const ShowWhatsAppService = async ( + id: string | number +): Promise<Whatsapp | undefined> => { + const whatsapp = await Whatsapp.findByPk(id); + + if (!whatsapp) { + throw new AppError("ERR_NO_WAPP_FOUND", 404); + } + + return whatsapp; +}; + +export default ShowWhatsAppService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import * as Yup from "yup"; + +import AppError from "../../errors/AppError"; +import Whatsapp from "../../models/Whatsapp"; + +interface WhatsappData { + name?: string; + status?: string; + isDefault?: boolean; +} + +interface Request { + whatsappData: WhatsappData; + whatsappId: string; +} + +const UpdateWhatsAppService = async ({ + whatsappData, + whatsappId +}: Request): Promise<Whatsapp> => { + const schema = Yup.object().shape({ + name: Yup.string().min(2), + default: Yup.boolean().test( + "Check-default", + "Only one default whatsapp is permited", + async value => { + if (value === true) { + const whatsappFound = await Whatsapp.findOne({ + where: { default: true } + }); + if (whatsappFound) { + return !(whatsappFound.id !== +whatsappId); + } + return true; + } + return true; + } + ) + }); + + const { name, status, isDefault } = whatsappData; + + try { + await schema.validate({ name, status, isDefault }); + } catch (err) { + throw new AppError(err.message); + } + + const whatsapp = await Whatsapp.findOne({ + where: { id: whatsappId } + }); + + if (!whatsapp) { + throw new AppError("ERR_NO_WAPP_FOUND", 404); + } + + await whatsapp.update({ + name, + status, + isDefault + }); + + return whatsapp; +}; + +export default UpdateWhatsAppService; + |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| CreateWhatsAppService.ts | +
+
+ |
+ 0% | +0/66 | +0% | +0/1 | +0% | +0/1 | +0% | +0/66 | +
| DeleteWhatsAppService.ts | +
+
+ |
+ 0% | +0/16 | +0% | +0/1 | +0% | +0/1 | +0% | +0/16 | +
| ListWhatsAppsService.ts | +
+
+ |
+ 0% | +0/9 | +0% | +0/1 | +0% | +0/1 | +0% | +0/9 | +
| ShowWhatsAppService.ts | +
+
+ |
+ 0% | +0/16 | +0% | +0/1 | +0% | +0/1 | +0% | +0/16 | +
| UpdateWhatsAppService.ts | +
+
+ |
+ 0% | +0/66 | +0% | +0/1 | +0% | +0/1 | +0% | +0/66 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| AuthServices | +
+
+ |
+ 0% | +0/41 | +0% | +0/1 | +0% | +0/1 | +0% | +0/41 | +
| ContactServices | +
+
+ |
+ 0% | +0/196 | +0% | +0/5 | +0% | +0/5 | +0% | +0/196 | +
| MessageServices | +
+
+ |
+ 0% | +0/103 | +0% | +0/2 | +0% | +0/2 | +0% | +0/103 | +
| SettingServices | +
+
+ |
+ 0% | +0/35 | +0% | +0/2 | +0% | +0/2 | +0% | +0/35 | +
| TicketServices | +
+
+ |
+ 0% | +0/294 | +0% | +0/5 | +0% | +0/5 | +0% | +0/294 | +
| UserServices | +
+
+ |
+ 0% | +0/276 | +0% | +0/6 | +0% | +0/6 | +0% | +0/276 | +
| WbotServices | +
+
+ |
+ 0% | +0/624 | +0% | +0/9 | +0% | +0/9 | +0% | +0/624 | +
| WhatsappService | +
+
+ |
+ 0% | +0/173 | +0% | +0/5 | +0% | +0/5 | +0% | +0/173 | +