improvement: code cleanup

This commit is contained in:
canove
2021-01-07 22:01:13 -03:00
parent 896f122cf7
commit c8c8dc43e9
9 changed files with 297 additions and 250 deletions

View File

@@ -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");
}
};

View File

@@ -15,6 +15,8 @@ const SetTicketMessagesAsRead = async (ticket: Ticket): Promise<void> => {
} }
); );
await ticket.update({ unreadMessages: 0 });
try { try {
const wbot = await GetTicketWbot(ticket); const wbot = await GetTicketWbot(ticket);
wbot.sendSeen(`${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`); wbot.sendSeen(`${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`);

View File

@@ -4,7 +4,7 @@ import { getIO } from "./socket";
import Whatsapp from "../models/Whatsapp"; import Whatsapp from "../models/Whatsapp";
import AppError from "../errors/AppError"; import AppError from "../errors/AppError";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
// import { handleMessage } from "../services/WbotServices/wbotMessageListener"; import { handleMessage } from "../services/WbotServices/wbotMessageListener";
interface Session extends Client { interface Session extends Client {
id?: number; id?: number;
@@ -12,20 +12,25 @@ interface Session extends Client {
const sessions: Session[] = []; const sessions: Session[] = [];
// const syncUnreadMessages = async (wbot: Session) => { const syncUnreadMessages = async (wbot: Session) => {
// const chats = await wbot.getChats(); const chats = await wbot.getChats();
// chats.forEach(async chat => { /* eslint-disable no-restricted-syntax */
// if (chat.unreadCount > 0) { /* eslint-disable no-await-in-loop */
// const unreadMessages = await chat.fetchMessages({ for (const chat of chats) {
// limit: chat.unreadCount if (chat.unreadCount > 0) {
// }); const unreadMessages = await chat.fetchMessages({
// unreadMessages.forEach(msg => { limit: chat.unreadCount
// handleMessage(msg, wbot); });
// });
// } for (const msg of unreadMessages) {
// }); await handleMessage(msg, wbot);
// }; }
await chat.sendSeen();
}
}
};
export const initWbot = async (whatsapp: Whatsapp): Promise<Session> => { export const initWbot = async (whatsapp: Whatsapp): Promise<Session> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -76,14 +81,16 @@ export const initWbot = async (whatsapp: Whatsapp): Promise<Session> => {
}); });
wbot.on("authenticated", async session => { wbot.on("authenticated", async session => {
logger.info("Session:", sessionName, "AUTHENTICATED"); logger.info(`Session: ${sessionName} AUTHENTICATED`);
await whatsapp.update({ await whatsapp.update({
session: JSON.stringify(session) session: JSON.stringify(session)
}); });
}); });
wbot.on("auth_failure", async msg => { 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) { if (whatsapp.retries > 1) {
await whatsapp.update({ session: "", retries: 0 }); await whatsapp.update({ session: "", retries: 0 });
@@ -106,8 +113,6 @@ export const initWbot = async (whatsapp: Whatsapp): Promise<Session> => {
wbot.on("ready", async () => { wbot.on("ready", async () => {
logger.info("Session:", sessionName, "READY"); logger.info("Session:", sessionName, "READY");
// syncUnreadMessages(wbot);
await whatsapp.update({ await whatsapp.update({
status: "CONNECTED", status: "CONNECTED",
qrcode: "", qrcode: "",
@@ -119,14 +124,15 @@ export const initWbot = async (whatsapp: Whatsapp): Promise<Session> => {
session: whatsapp session: whatsapp
}); });
wbot.sendPresenceAvailable();
const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id); const sessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
if (sessionIndex === -1) { if (sessionIndex === -1) {
wbot.id = whatsapp.id; wbot.id = whatsapp.id;
sessions.push(wbot); sessions.push(wbot);
} }
wbot.sendPresenceAvailable();
syncUnreadMessages(wbot);
resolve(wbot); resolve(wbot);
}); });
} catch (err) { } catch (err) {

View File

@@ -4,14 +4,11 @@ import {
CreatedAt, CreatedAt,
UpdatedAt, UpdatedAt,
Model, Model,
DataType,
PrimaryKey, PrimaryKey,
ForeignKey, ForeignKey,
BelongsTo, BelongsTo,
HasMany, HasMany,
AutoIncrement, AutoIncrement,
AfterFind,
BeforeUpdate,
Default Default
} from "sequelize-typescript"; } from "sequelize-typescript";
@@ -30,7 +27,7 @@ class Ticket extends Model<Ticket> {
@Column({ defaultValue: "pending" }) @Column({ defaultValue: "pending" })
status: string; status: string;
@Column(DataType.VIRTUAL) @Column
unreadMessages: number; unreadMessages: number;
@Column @Column
@@ -69,26 +66,6 @@ class Ticket extends Model<Ticket> {
@HasMany(() => Message) @HasMany(() => Message)
messages: Message[]; messages: Message[];
@AfterFind
static async countTicketsUnreadMessages(tickets: Ticket[]): Promise<void> {
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<void> {
ticket.unreadMessages = await Message.count({
where: { ticketId: ticket.id, read: false }
});
}
} }
export default Ticket; export default Ticket;

View File

@@ -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<Contact> => {
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;

View File

@@ -1,6 +1,6 @@
import AppError from "../../errors/AppError"; import { getIO } from "../../libs/socket";
import Message from "../../models/Message"; import Message from "../../models/Message";
import ShowTicketService from "../TicketServices/ShowTicketService"; import Ticket from "../../models/Ticket";
interface MessageData { interface MessageData {
id: string; id: string;
@@ -19,17 +19,16 @@ interface Request {
const CreateMessageService = async ({ const CreateMessageService = async ({
messageData messageData
}: Request): Promise<Message> => { }: Request): Promise<Message> => {
const ticket = await ShowTicketService(messageData.ticketId);
if (!ticket) {
throw new AppError("ERR_NO_TICKET_FOUND", 404);
}
await Message.upsert(messageData); await Message.upsert(messageData);
const message = await Message.findByPk(messageData.id, { const message = await Message.findByPk(messageData.id, {
include: [ include: [
"contact", "contact",
{
model: Ticket,
as: "ticket",
include: ["contact"]
},
{ {
model: Message, model: Message,
as: "quotedMsg", as: "quotedMsg",
@@ -39,9 +38,20 @@ const CreateMessageService = async ({
}); });
if (!message) { 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; return message;
}; };

View File

@@ -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<Ticket> => {
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;

View File

@@ -1,8 +1,6 @@
import { join } from "path"; import { join } from "path";
import { promisify } from "util"; import { promisify } from "util";
import { writeFile } from "fs"; import { writeFile } from "fs";
import { Op } from "sequelize";
import { subHours } from "date-fns";
import * as Sentry from "@sentry/node"; import * as Sentry from "@sentry/node";
import { import {
@@ -17,10 +15,10 @@ import Ticket from "../../models/Ticket";
import Message from "../../models/Message"; import Message from "../../models/Message";
import { getIO } from "../../libs/socket"; import { getIO } from "../../libs/socket";
import AppError from "../../errors/AppError";
import ShowTicketService from "../TicketServices/ShowTicketService";
import CreateMessageService from "../MessageServices/CreateMessageService"; import CreateMessageService from "../MessageServices/CreateMessageService";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import CreateOrUpdateContactService from "../ContactServices/CreateOrUpdateContactService";
import FindOrCreateTicketService from "../TicketServices/FindOrCreateTicketService";
interface Session extends Client { interface Session extends Client {
id?: number; id?: number;
@@ -28,141 +26,48 @@ interface Session extends Client {
const writeFileAsync = promisify(writeFile); const writeFileAsync = promisify(writeFile);
const verifyContact = async ( const verifyContact = async (msgContact: WbotContact): Promise<Contact> => {
msgContact: WbotContact, const profilePicUrl = await msgContact.getProfilePicUrl();
profilePicUrl: string
): Promise<Contact> => {
const io = getIO();
let contact = await Contact.findOne({ const contactData = {
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, name: msgContact.name || msgContact.pushname || msgContact.id.user,
number: msgContact.id.user, number: msgContact.id.user,
profilePicUrl profilePicUrl,
}); isGroup: msgContact.isGroup
};
io.emit("contact", { const contact = CreateOrUpdateContactService(contactData);
action: "create",
contact
});
}
return contact; return contact;
}; };
const verifyGroup = async (msgGroupContact: WbotContact) => { const verifyQuotedMessage = async (
const profilePicUrl = await msgGroupContact.getProfilePicUrl(); msg: WbotMessage
): Promise<Message | null> => {
if (!msg.hasQuotedMsg) return null;
let groupContact = await Contact.findOne({ const wbotQuotedMsg = await msg.getQuotedMessage();
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 quotedMsg = await Message.findOne({
where: { id: wbotQuotedMsg.id.id }
});
if (!quotedMsg) return null;
return quotedMsg;
}; };
const verifyTicket = async ( const verifyMediaMessage = 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: {
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;
};
const verifyMedia = async (
msg: WbotMessage, msg: WbotMessage,
ticket: Ticket, ticket: Ticket,
contact: Contact contact: Contact
): Promise<Message> => { ): Promise<Message> => {
let quotedMsg: Message | null = null; const quotedMsg = await verifyQuotedMessage(msg);
const media = await msg.downloadMedia(); const media = await msg.downloadMedia();
if (!media) { if (!media) {
throw new AppError("ERR_WAPP_DOWNLOAD_MEDIA"); throw new Error("ERR_WAPP_DOWNLOAD_MEDIA");
}
if (msg.hasQuotedMsg) {
const wbotQuotedMsg = await msg.getQuotedMessage();
quotedMsg = await Message.findByPk(wbotQuotedMsg.id.id);
} }
if (!media.filename) { if (!media.filename) {
@@ -194,8 +99,8 @@ const verifyMedia = async (
}; };
const newMessage = await CreateMessageService({ messageData }); const newMessage = await CreateMessageService({ messageData });
await ticket.update({ lastMessage: msg.body || media.filename }); await ticket.update({ lastMessage: msg.body || media.filename });
return newMessage; return newMessage;
}; };
@@ -204,18 +109,8 @@ const verifyMessage = async (
ticket: Ticket, ticket: Ticket,
contact: Contact contact: Contact
) => { ) => {
let newMessage: Message | null; const quotedMsg = await verifyQuotedMessage(msg);
let quotedMsg: Message | null = null;
if (msg.hasQuotedMsg) {
const wbotQuotedMsg = await msg.getQuotedMessage();
quotedMsg = await Message.findByPk(wbotQuotedMsg.id.id);
}
if (msg.hasMedia) {
newMessage = await verifyMedia(msg, ticket, contact);
} else {
const messageData = { const messageData = {
id: msg.id.id, id: msg.id.id,
ticketId: ticket.id, ticketId: ticket.id,
@@ -227,20 +122,8 @@ const verifyMessage = async (
quotedMsgId: quotedMsg?.id quotedMsgId: quotedMsg?.id
}; };
newMessage = await CreateMessageService({ messageData }); await CreateMessageService({ messageData });
await ticket.update({ lastMessage: msg.body }); 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 => { const isValidMsg = (msg: WbotMessage): boolean => {
@@ -263,6 +146,8 @@ const handleMessage = async (
msg: WbotMessage, msg: WbotMessage,
wbot: Session wbot: Session
): Promise<void> => { ): Promise<void> => {
return new Promise<void>((resolve, reject) => {
(async () => {
if (!isValidMsg(msg)) { if (!isValidMsg(msg)) {
return; return;
} }
@@ -272,14 +157,13 @@ const handleMessage = async (
let groupContact: Contact | undefined; let groupContact: Contact | undefined;
if (msg.fromMe) { 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" // 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 // in this case, return and let this message be handled by "media_uploaded" event, when it will have "hasMedia = true"
// 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; if (!msg.hasMedia && msg.type !== "chat" && msg.type !== "vcard")
return;
msgContact = await wbot.getContactById(msg.to);
} else { } else {
msgContact = await msg.getContact(); msgContact = await msg.getContact();
} }
@@ -295,18 +179,33 @@ const handleMessage = async (
msgGroupContact = await wbot.getContactById(msg.from); msgGroupContact = await wbot.getContactById(msg.from);
} }
groupContact = await verifyGroup(msgGroupContact); groupContact = await verifyContact(msgGroupContact);
} }
const profilePicUrl = await msgContact.getProfilePicUrl(); const contact = await verifyContact(msgContact);
const contact = await verifyContact(msgContact, profilePicUrl); const ticket = await FindOrCreateTicketService(
const ticket = await verifyTicket(contact, wbot.id!, groupContact); 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); await verifyMessage(msg, ticket, contact);
} catch (err) { } catch (err) {
Sentry.captureException(err); Sentry.captureException(err);
logger.error(err); logger.error(`Error handling whatsapp message: Err: ${err}`);
reject(err);
} }
})();
});
}; };
const handleMsgAck = async (msg: WbotMessage, ack: MessageAck) => { const handleMsgAck = async (msg: WbotMessage, ack: MessageAck) => {
@@ -336,7 +235,7 @@ const handleMsgAck = async (msg: WbotMessage, ack: MessageAck) => {
}); });
} catch (err) { } catch (err) {
Sentry.captureException(err); Sentry.captureException(err);
logger.log(err); logger.error(`Error handling message ack. Err: ${err}`);
} }
}; };

View File

@@ -19,7 +19,7 @@ const wbotMonitor = async (
try { try {
wbot.on("change_state", async newState => { wbot.on("change_state", async newState => {
logger.info("Monitor session:", sessionName, newState); logger.info(`Monitor session: ${sessionName}, ${newState}`);
try { try {
await whatsapp.update({ status: newState }); await whatsapp.update({ status: newState });
} catch (err) { } catch (err) {
@@ -53,7 +53,7 @@ const wbotMonitor = async (
}); });
wbot.on("disconnected", async reason => { wbot.on("disconnected", async reason => {
logger.info("Disconnected session:", sessionName, reason); logger.info(`Disconnected session: ${sessionName}, reason: ${reason}`);
try { try {
await whatsapp.update({ status: "OPENING", session: "" }); await whatsapp.update({ status: "OPENING", session: "" });
} catch (err) { } catch (err) {