feat: start adding auto reply

This commit is contained in:
canove
2021-01-09 18:02:54 -03:00
parent 3c6d0660d1
commit cca253cd0a
17 changed files with 272 additions and 94 deletions

View File

@@ -11,9 +11,9 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
}; };
export const store = async (req: Request, res: Response): Promise<Response> => { export const store = async (req: Request, res: Response): Promise<Response> => {
const { name, color } = req.body; const { name, color, greetingMessage } = req.body;
const queue = await Queue.create({ name, color }); const queue = await Queue.create({ name, color, greetingMessage });
return res.status(200).json(queue); return res.status(200).json(queue);
}; };

View File

@@ -9,9 +9,14 @@ import ListWhatsAppsService from "../services/WhatsappService/ListWhatsAppsServi
import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService"; import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService";
import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppService"; import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppService";
interface QueueData {
id: number;
optionNumber: number;
}
interface WhatsappData { interface WhatsappData {
name: string; name: string;
queueIds: number[]; queuesData: QueueData[];
greetingMessage?: string;
status?: string; status?: string;
isDefault?: boolean; isDefault?: boolean;
} }
@@ -23,13 +28,20 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
}; };
export const store = async (req: Request, res: Response): Promise<Response> => { export const store = async (req: Request, res: Response): Promise<Response> => {
const { name, status, isDefault, queueIds }: WhatsappData = req.body; const {
name,
status,
isDefault,
greetingMessage,
queuesData
}: WhatsappData = req.body;
const { whatsapp, oldDefaultWhatsapp } = await CreateWhatsAppService({ const { whatsapp, oldDefaultWhatsapp } = await CreateWhatsAppService({
name, name,
status, status,
isDefault, isDefault,
queueIds greetingMessage,
queuesData
}); });
// StartWhatsAppSession(whatsapp); // StartWhatsAppSession(whatsapp);

View File

@@ -19,6 +19,9 @@ module.exports = {
allowNull: false, allowNull: false,
unique: true unique: true
}, },
greetingMessage: {
type: DataTypes.TEXT
},
createdAt: { createdAt: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: false allowNull: false

View File

@@ -3,6 +3,9 @@ import { QueryInterface, DataTypes } from "sequelize";
module.exports = { module.exports = {
up: (queryInterface: QueryInterface) => { up: (queryInterface: QueryInterface) => {
return queryInterface.createTable("WhatsappQueues", { return queryInterface.createTable("WhatsappQueues", {
optionNumber: {
type: DataTypes.INTEGER
},
whatsappId: { whatsappId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true primaryKey: true

View File

@@ -0,0 +1,13 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Whatsapps", "greetingMessage", {
type: DataTypes.TEXT
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Whatsapps", "greetingMessage");
}
};

View File

@@ -8,7 +8,8 @@ import {
AutoIncrement, AutoIncrement,
AllowNull, AllowNull,
Unique, Unique,
BelongsToMany BelongsToMany,
HasMany
} from "sequelize-typescript"; } from "sequelize-typescript";
import User from "./User"; import User from "./User";
import UserQueue from "./UserQueue"; import UserQueue from "./UserQueue";
@@ -33,6 +34,9 @@ class Queue extends Model<Queue> {
@Column @Column
color: string; color: string;
@Column
greetingMessage: string;
@CreatedAt @CreatedAt
createdAt: Date; createdAt: Date;

View File

@@ -47,6 +47,9 @@ class Whatsapp extends Model<Whatsapp> {
@Column @Column
retries: number; retries: number;
@Column(DataType.TEXT)
greetingMessage: string;
@Default(false) @Default(false)
@AllowNull @AllowNull
@Column @Column
@@ -63,6 +66,9 @@ class Whatsapp extends Model<Whatsapp> {
@BelongsToMany(() => Queue, () => WhatsappQueue) @BelongsToMany(() => Queue, () => WhatsappQueue)
queues: Array<Queue & { WhatsappQueue: WhatsappQueue }>; queues: Array<Queue & { WhatsappQueue: WhatsappQueue }>;
@HasMany(() => WhatsappQueue)
whatsappQueues: WhatsappQueue[];
} }
export default Whatsapp; export default Whatsapp;

View File

@@ -4,13 +4,17 @@ import {
CreatedAt, CreatedAt,
UpdatedAt, UpdatedAt,
Model, Model,
ForeignKey ForeignKey,
BelongsTo
} from "sequelize-typescript"; } from "sequelize-typescript";
import Queue from "./Queue"; import Queue from "./Queue";
import Whatsapp from "./Whatsapp"; import Whatsapp from "./Whatsapp";
@Table @Table
class WhatsappQueue extends Model<WhatsappQueue> { class WhatsappQueue extends Model<WhatsappQueue> {
@Column
optionNumber: number;
@ForeignKey(() => Whatsapp) @ForeignKey(() => Whatsapp)
@Column @Column
whatsappId: number; whatsappId: number;
@@ -24,6 +28,9 @@ class WhatsappQueue extends Model<WhatsappQueue> {
@UpdatedAt @UpdatedAt
updatedAt: Date; updatedAt: Date;
@BelongsTo(() => Queue)
queue: Queue;
} }
export default WhatsappQueue; export default WhatsappQueue;

View File

@@ -0,0 +1,32 @@
import Whatsapp from "../../models/Whatsapp";
import WhatsappQueue from "../../models/WhatsappQueue";
interface QueueData {
id: number;
optionNumber: number;
}
const AssociateWhatsappQueue = async (
whatsapp: Whatsapp,
queuesData: QueueData[]
): Promise<void> => {
const queueIds = queuesData.map(({ id }) => id);
await whatsapp.$set("queues", queueIds);
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-await-in-loop */
for (const queueData of queuesData) {
await WhatsappQueue.update(
{ optionNumber: queueData.optionNumber },
{
where: {
whatsappId: whatsapp.id,
queueId: queueData.id
}
}
);
}
};
export default AssociateWhatsappQueue;

View File

@@ -16,23 +16,19 @@ const FindOrCreateTicketService = async (
[Op.or]: ["open", "pending"] [Op.or]: ["open", "pending"]
}, },
contactId: groupContact ? groupContact.id : contact.id contactId: groupContact ? groupContact.id : contact.id
}, }
include: ["contact"]
}); });
if (ticket) { if (ticket) {
await ticket.update({ unreadMessages }); await ticket.update({ unreadMessages });
return ticket;
} }
if (groupContact) { if (!ticket && groupContact) {
ticket = await Ticket.findOne({ ticket = await Ticket.findOne({
where: { where: {
contactId: groupContact.id contactId: groupContact.id
}, },
order: [["updatedAt", "DESC"]], order: [["updatedAt", "DESC"]]
include: ["contact"]
}); });
if (ticket) { if (ticket) {
@@ -41,10 +37,10 @@ const FindOrCreateTicketService = async (
userId: null, userId: null,
unreadMessages unreadMessages
}); });
return ticket;
} }
} else { }
if (!ticket && !groupContact) {
ticket = await Ticket.findOne({ ticket = await Ticket.findOne({
where: { where: {
updatedAt: { updatedAt: {
@@ -52,8 +48,7 @@ const FindOrCreateTicketService = async (
}, },
contactId: contact.id contactId: contact.id
}, },
order: [["updatedAt", "DESC"]], order: [["updatedAt", "DESC"]]
include: ["contact"]
}); });
if (ticket) { if (ticket) {
@@ -62,20 +57,20 @@ const FindOrCreateTicketService = async (
userId: null, userId: null,
unreadMessages unreadMessages
}); });
return ticket;
} }
} }
const { id } = await Ticket.create({ if (!ticket) {
contactId: groupContact ? groupContact.id : contact.id, ticket = await Ticket.create({
status: "pending", contactId: groupContact ? groupContact.id : contact.id,
isGroup: !!groupContact, status: "pending",
unreadMessages, isGroup: !!groupContact,
whatsappId unreadMessages,
}); whatsappId
});
}
ticket = await ShowTicketService(id); ticket = await ShowTicketService(ticket.id);
return ticket; return ticket;
}; };

View File

@@ -2,6 +2,7 @@ import Ticket from "../../models/Ticket";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import Contact from "../../models/Contact"; import Contact from "../../models/Contact";
import User from "../../models/User"; import User from "../../models/User";
import Queue from "../../models/Queue";
const ShowTicketService = async (id: string | number): Promise<Ticket> => { const ShowTicketService = async (id: string | number): Promise<Ticket> => {
const ticket = await Ticket.findByPk(id, { const ticket = await Ticket.findByPk(id, {
@@ -16,6 +17,11 @@ const ShowTicketService = async (id: string | number): Promise<Ticket> => {
model: User, model: User,
as: "user", as: "user",
attributes: ["id", "name"] attributes: ["id", "name"]
},
{
model: Queue,
as: "queue",
attributes: ["id", "name", "color"]
} }
] ]
}); });

View File

@@ -1,8 +1,8 @@
import Whatsapp from "../../models/Whatsapp"; import ListWhatsAppsService from "../WhatsappService/ListWhatsAppsService";
import { StartWhatsAppSession } from "./StartWhatsAppSession"; import { StartWhatsAppSession } from "./StartWhatsAppSession";
export const StartAllWhatsAppsSessions = async (): Promise<void> => { export const StartAllWhatsAppsSessions = async (): Promise<void> => {
const whatsapps = await Whatsapp.findAll(); const whatsapps = await ListWhatsAppsService();
if (whatsapps.length > 0) { if (whatsapps.length > 0) {
whatsapps.forEach(whatsapp => { whatsapps.forEach(whatsapp => {
StartWhatsAppSession(whatsapp); StartWhatsAppSession(whatsapp);

View File

@@ -19,6 +19,7 @@ import CreateMessageService from "../MessageServices/CreateMessageService";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import CreateOrUpdateContactService from "../ContactServices/CreateOrUpdateContactService"; import CreateOrUpdateContactService from "../ContactServices/CreateOrUpdateContactService";
import FindOrCreateTicketService from "../TicketServices/FindOrCreateTicketService"; import FindOrCreateTicketService from "../TicketServices/FindOrCreateTicketService";
import ShowWhatsAppService from "../WhatsappService/ShowWhatsAppService";
interface Session extends Client { interface Session extends Client {
id?: number; id?: number;
@@ -126,6 +127,54 @@ const verifyMessage = async (
await CreateMessageService({ messageData }); await CreateMessageService({ messageData });
}; };
const verifyQueue = async (
wbot: Session,
msg: WbotMessage,
ticket: Ticket,
contact: Contact
) => {
const { whatsappQueues, greetingMessage } = await ShowWhatsAppService(
wbot.id!
);
if (whatsappQueues.length === 1) {
await ticket.$set("queue", whatsappQueues[0].queue);
// TODO sendTicketQueueUpdate to frontend
return;
}
const selectedOption = msg.body[0];
const validOption = whatsappQueues.find(
q => q.optionNumber === +selectedOption
);
if (validOption) {
await ticket.$set("queue", validOption.queue);
const body = `\u200e ${validOption.queue.greetingMessage}`;
const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body);
await verifyMessage(sentMessage, ticket, contact);
// TODO sendTicketQueueUpdate to frontend
} else {
let options = "";
whatsappQueues.forEach(whatsQueue => {
options += `*${whatsQueue.optionNumber}* - ${whatsQueue.queue.name}\n`;
});
const body = `\u200e ${greetingMessage}\n\n${options}`;
const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body);
await verifyMessage(sentMessage, ticket, contact);
}
};
const isValidMsg = (msg: WbotMessage): boolean => { const isValidMsg = (msg: WbotMessage): boolean => {
if (msg.from === "status@broadcast") return false; if (msg.from === "status@broadcast") return false;
if ( if (
@@ -146,64 +195,64 @@ const handleMessage = async (
msg: WbotMessage, msg: WbotMessage,
wbot: Session wbot: Session
): Promise<void> => { ): Promise<void> => {
return new Promise<void>((resolve, reject) => { if (!isValidMsg(msg)) {
(async () => { return;
if (!isValidMsg(msg)) { }
return;
try {
let msgContact: WbotContact;
let groupContact: Contact | undefined;
if (msg.fromMe) {
// messages sent automatically by wbot have a special character in front of it
// if so, this message was already been stored in database;
if (/\u200e/.test(msg.body[0])) return;
// media messages sent from me from cell phone, first comes with "hasMedia = false" and type = "image/ptt/etc"
// in this case, return and let this message be handled by "media_uploaded" event, when it will have "hasMedia = true"
if (!msg.hasMedia && msg.type !== "chat" && msg.type !== "vcard") return;
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);
} }
try { groupContact = await verifyContact(msgGroupContact);
let msgContact: WbotContact; }
let groupContact: Contact | undefined;
if (msg.fromMe) { const contact = await verifyContact(msgContact);
// media messages sent from me from cell phone, first comes with "hasMedia = false" and type = "image/ptt/etc" const ticket = await FindOrCreateTicketService(
// in this case, return and let this message be handled by "media_uploaded" event, when it will have "hasMedia = true" contact,
wbot.id!,
chat.unreadCount,
groupContact
);
if (!msg.hasMedia && msg.type !== "chat" && msg.type !== "vcard") if (msg.hasMedia) {
return; await verifyMediaMessage(msg, ticket, contact);
} else {
await verifyMessage(msg, ticket, contact);
}
msgContact = await wbot.getContactById(msg.to); if (!ticket.queue && !chat.isGroup && !msg.fromMe) {
} else { await verifyQueue(wbot, msg, ticket, contact);
msgContact = await msg.getContact(); }
} } catch (err) {
Sentry.captureException(err);
const chat = await msg.getChat(); logger.error(`Error handling whatsapp message: Err: ${err}`);
}
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();
}
} catch (err) {
Sentry.captureException(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) => {

View File

@@ -2,10 +2,16 @@ import * as Yup from "yup";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import Whatsapp from "../../models/Whatsapp"; import Whatsapp from "../../models/Whatsapp";
import AssociateWhatsappQueue from "../QueueService/AssociateWhatsappQueue";
interface QueueData {
id: number;
optionNumber: number;
}
interface Request { interface Request {
name: string; name: string;
queueIds: number[]; queuesData: QueueData[];
greetingMessage?: string;
status?: string; status?: string;
isDefault?: boolean; isDefault?: boolean;
} }
@@ -18,7 +24,8 @@ interface Response {
const CreateWhatsAppService = async ({ const CreateWhatsAppService = async ({
name, name,
status = "OPENING", status = "OPENING",
queueIds = [], queuesData = [],
greetingMessage,
isDefault = false isDefault = false
}: Request): Promise<Response> => { }: Request): Promise<Response> => {
const schema = Yup.object().shape({ const schema = Yup.object().shape({
@@ -68,12 +75,13 @@ const CreateWhatsAppService = async ({
{ {
name, name,
status, status,
greetingMessage,
isDefault isDefault
}, },
{ include: ["queues"] } { include: ["queues"] }
); );
await whatsapp.$set("queues", queueIds); await AssociateWhatsappQueue(whatsapp, queuesData);
await whatsapp.reload(); await whatsapp.reload();

View File

@@ -1,11 +1,24 @@
import Queue from "../../models/Queue"; import Queue from "../../models/Queue";
import Whatsapp from "../../models/Whatsapp"; import Whatsapp from "../../models/Whatsapp";
import WhatsappQueue from "../../models/WhatsappQueue";
const ListWhatsAppsService = async (): Promise<Whatsapp[]> => { const ListWhatsAppsService = async (): Promise<Whatsapp[]> => {
const whatsapps = await Whatsapp.findAll({ const whatsapps = await Whatsapp.findAll({
include: [ include: [
{ model: Queue, as: "queues", attributes: ["id", "name", "color"] } {
] model: WhatsappQueue,
as: "whatsappQueues",
attributes: ["optionNumber"],
include: [
{
model: Queue,
as: "queue",
attributes: ["id", "name", "color", "greetingMessage"]
}
]
}
],
order: [["whatsappQueues", "optionNumber", "ASC"]]
}); });
return whatsapps; return whatsapps;

View File

@@ -1,12 +1,25 @@
import Whatsapp from "../../models/Whatsapp"; import Whatsapp from "../../models/Whatsapp";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import Queue from "../../models/Queue"; import Queue from "../../models/Queue";
import WhatsappQueue from "../../models/WhatsappQueue";
const ShowWhatsAppService = async (id: string | number): Promise<Whatsapp> => { const ShowWhatsAppService = async (id: string | number): Promise<Whatsapp> => {
const whatsapp = await Whatsapp.findByPk(id, { const whatsapp = await Whatsapp.findByPk(id, {
include: [ include: [
{ model: Queue, as: "queues", attributes: ["id", "name", "color"] } {
] model: WhatsappQueue,
as: "whatsappQueues",
attributes: ["optionNumber"],
include: [
{
model: Queue,
as: "queue",
attributes: ["id", "name", "color", "greetingMessage"]
}
]
}
],
order: [["whatsappQueues", "optionNumber", "ASC"]]
}); });
if (!whatsapp) { if (!whatsapp) {

View File

@@ -4,13 +4,19 @@ import { Op } from "sequelize";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import Whatsapp from "../../models/Whatsapp"; import Whatsapp from "../../models/Whatsapp";
import ShowWhatsAppService from "./ShowWhatsAppService"; import ShowWhatsAppService from "./ShowWhatsAppService";
import AssociateWhatsappQueue from "../QueueService/AssociateWhatsappQueue";
interface QueueData {
id: number;
optionNumber: number;
}
interface WhatsappData { interface WhatsappData {
name?: string; name?: string;
status?: string; status?: string;
session?: string; session?: string;
isDefault?: boolean; isDefault?: boolean;
queueIds?: number[]; greetingMessage?: string;
queuesData?: QueueData[];
} }
interface Request { interface Request {
@@ -32,7 +38,14 @@ const UpdateWhatsAppService = async ({
isDefault: Yup.boolean() isDefault: Yup.boolean()
}); });
const { name, status, isDefault, session, queueIds = [] } = whatsappData; const {
name,
status,
isDefault,
session,
greetingMessage,
queuesData = []
} = whatsappData;
try { try {
await schema.validate({ name, status, isDefault }); await schema.validate({ name, status, isDefault });
@@ -57,10 +70,11 @@ const UpdateWhatsAppService = async ({
name, name,
status, status,
session, session,
greetingMessage,
isDefault isDefault
}); });
await whatsapp.$set("queues", queueIds); await AssociateWhatsappQueue(whatsapp, queuesData);
await whatsapp.reload(); await whatsapp.reload();