From e91b13a90cb08ede4469fd0caf8e2aa6f99fa7a8 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 14 Mar 2020 15:40:37 -0300 Subject: [PATCH 01/71] WhatsPro --- package.json | 3 ++- src/data/dataSources/integrations.ts | 4 ++++ src/data/resolvers/mutations/conversations.ts | 4 ++++ src/data/resolvers/mutations/integrations.ts | 1 + src/db/models/definitions/constants.ts | 2 ++ yarn.lock | 8 -------- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index b4a8e8fcb..ad3780d09 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "start-crons": "node dist/cronJobs", "start-workers": "node dist/workers", "dev": "DEBUG=erxes-api:* NODE_ENV=development node_modules/.bin/ts-node-dev --respawn --ignore-watch node_modules -- src", + "dev-win": "set DEBUG=erxes-api:* & set NODE_ENV=development & node_modules/.bin/ts-node-dev --respawn --ignore-watch node_modules -- src", "dev-crons": "PROCESS_NAME=crons DEBUG=erxes-crons:* NODE_ENV=development node_modules/.bin/ts-node-dev --respawn src/cronJobs", "dev-workers": "PROCESS_NAME=workers DEBUG=erxes-workers:* NODE_ENV=development node_modules/.bin/ts-node-dev --experimental-worker --respawn src/workers", "build": "tsc -p tsconfig.prod.json && cp -rf src/private dist/ && yarn generateVersion", @@ -133,6 +134,6 @@ }, "snyk": true, "engines": { - "node": "10.x.x" + "node": "12.x.x" } } diff --git a/src/data/dataSources/integrations.ts b/src/data/dataSources/integrations.ts index e36b99d82..ee84e1fdb 100644 --- a/src/data/dataSources/integrations.ts +++ b/src/data/dataSources/integrations.ts @@ -66,4 +66,8 @@ export default class IntegrationsAPI extends RESTDataSource { public async updateConfigs(configsMap) { return this.post('/update-configs', { configsMap }); } + + public async replyWhatsPro(params) { + return this.post('/whatsPro/reply', params); + } } diff --git a/src/data/resolvers/mutations/conversations.ts b/src/data/resolvers/mutations/conversations.ts index 2efafc0d5..368471753 100644 --- a/src/data/resolvers/mutations/conversations.ts +++ b/src/data/resolvers/mutations/conversations.ts @@ -266,6 +266,10 @@ const conversationMutations = { requestName = 'replyTwitterDm'; } + if (kind === KIND_CHOICES.WHATSPRO) { + requestName = 'replyWhatsPro'; + } + await sendConversationToIntegrations(type, integrationId, conversationId, requestName, doc, dataSources, action); const dbMessage = await ConversationMessages.getMessage(message._id); diff --git a/src/data/resolvers/mutations/integrations.ts b/src/data/resolvers/mutations/integrations.ts index e6662ef41..68bff57e6 100644 --- a/src/data/resolvers/mutations/integrations.ts +++ b/src/data/resolvers/mutations/integrations.ts @@ -188,6 +188,7 @@ const integrationMutations = { 'nylas-yahoo', 'chatfuel', 'twitter-dm', + 'whatspro', ].includes(integration.kind) ) { await dataSources.IntegrationsAPI.removeIntegration({ integrationId: _id }); diff --git a/src/db/models/definitions/constants.ts b/src/db/models/definitions/constants.ts index 6e1a4329d..427fa0d9d 100644 --- a/src/db/models/definitions/constants.ts +++ b/src/db/models/definitions/constants.ts @@ -66,6 +66,7 @@ export const KIND_CHOICES = { CALLPRO: 'callpro', TWITTER_DM: 'twitter-dm', CHATFUEL: 'chatfuel', + WHATSPRO: 'whatspro', ALL: [ 'messenger', 'lead', @@ -80,6 +81,7 @@ export const KIND_CHOICES = { 'nylas-outlook', 'nylas-yahoo', 'twitter-dm', + 'whatspro', ], }; diff --git a/yarn.lock b/yarn.lock index 9aa63c197..c897f808c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7305,14 +7305,6 @@ log-update@^1.0.2: ansi-escapes "^1.0.0" cli-cursor "^1.0.2" -<<<<<<< HEAD -======= -loglevel@^1.6.1: - version "1.6.7" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.7.tgz#b3e034233188c68b889f5b862415306f565e2c56" - integrity sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A== - ->>>>>>> 989625adc7b09d5661277f9b916dcef0e5f1ae4e lolex@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7" From 3914bd767fadcecacc8b53c48db2f59f6769b20a Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 14 Mar 2020 16:42:20 -0300 Subject: [PATCH 02/71] ChatBot --- src/commands/initProject.ts | 143 +++++++++- src/data/constants.ts | 3 + src/data/modules/flow/index.ts | 269 ++++++++++++++++++ .../modules/integrations/receiveMessage.ts | 3 + src/data/resolvers/flowActionTypes.ts | 1 + src/data/resolvers/flowActions.ts | 1 + src/data/resolvers/flows.ts | 8 + src/data/resolvers/index.ts | 7 + src/data/resolvers/mutations/conversations.ts | 1 + .../resolvers/mutations/flowActionTypes.ts | 65 +++++ src/data/resolvers/mutations/flowActions.ts | 65 +++++ src/data/resolvers/mutations/flows.ts | 71 +++++ src/data/resolvers/mutations/index.ts | 6 + src/data/resolvers/queries/flowActionTypes.ts | 61 ++++ src/data/resolvers/queries/flowActions.ts | 61 ++++ src/data/resolvers/queries/flows.ts | 61 ++++ src/data/resolvers/queries/index.ts | 6 + src/data/resolvers/queries/logs.ts | 15 + src/data/schema/conversation.ts | 2 + src/data/schema/flow.ts | 25 ++ src/data/schema/flowAction.ts | 24 ++ src/data/schema/flowActionType.ts | 22 ++ src/data/schema/index.ts | 17 ++ src/data/schema/integration.ts | 5 +- src/data/types.ts | 3 + src/db/factories.ts | 51 ++++ src/db/models/FlowActionTypes.ts | 59 ++++ src/db/models/FlowActions.ts | 59 ++++ src/db/models/Flows.ts | 69 +++++ .../definitions/conversationMessages.ts | 2 + src/db/models/definitions/conversations.ts | 4 + src/db/models/definitions/flowActionTypes.ts | 23 ++ src/db/models/definitions/flowActions.ts | 44 +++ src/db/models/definitions/flows.ts | 24 ++ src/db/models/definitions/integrations.ts | 2 + src/db/models/index.ts | 6 + 36 files changed, 1276 insertions(+), 12 deletions(-) create mode 100644 src/data/modules/flow/index.ts create mode 100644 src/data/resolvers/flowActionTypes.ts create mode 100644 src/data/resolvers/flowActions.ts create mode 100644 src/data/resolvers/flows.ts create mode 100644 src/data/resolvers/mutations/flowActionTypes.ts create mode 100644 src/data/resolvers/mutations/flowActions.ts create mode 100644 src/data/resolvers/mutations/flows.ts create mode 100644 src/data/resolvers/queries/flowActionTypes.ts create mode 100644 src/data/resolvers/queries/flowActions.ts create mode 100644 src/data/resolvers/queries/flows.ts create mode 100644 src/data/schema/flow.ts create mode 100644 src/data/schema/flowAction.ts create mode 100644 src/data/schema/flowActionType.ts create mode 100644 src/db/models/FlowActionTypes.ts create mode 100644 src/db/models/FlowActions.ts create mode 100644 src/db/models/Flows.ts create mode 100644 src/db/models/definitions/flowActionTypes.ts create mode 100644 src/db/models/definitions/flowActions.ts create mode 100644 src/db/models/definitions/flows.ts diff --git a/src/commands/initProject.ts b/src/commands/initProject.ts index e37d0513c..4a76cf484 100644 --- a/src/commands/initProject.ts +++ b/src/commands/initProject.ts @@ -1,5 +1,5 @@ import { connect, disconnect } from '../db/connection'; -import { Users } from '../db/models'; +import { Users, FlowActionTypes, Flows, FlowActions } from '../db/models'; connect() .then(async () => { @@ -13,19 +13,140 @@ connect() }); // create admin user - const user = await Users.createUser({ - username: 'admin', - password: newPwd, - email: 'admin@erxes.io', - isOwner: true, - details: { - fullName: 'Admin', - }, + + let user = await Users.findOne({ email: 'admin@erxes.io' }); + + if (!user) { + user = await Users.createUser({ + username: 'admin', + password: newPwd, + email: 'admin@erxes.io', + isOwner: true, + details: { + fullName: 'Admin', + }, + }); + + console.log('\x1b[32m%s\x1b[0m', 'Your new password: ' + newPwd); + + return Users.findOne({ _id: user._id }); + } + + return; + }) + + .then(async () => { + let flowActionTypes = await FlowActionTypes.find({}); + + if (!flowActionTypes || !flowActionTypes.length) { + flowActionTypes = [ + 'erxes.action.define.department', + 'erxes.action.define.tabulation', + 'erxes.action.transfer.to.agent', + 'erxes.action.put.in.queue', + 'erxes.action.finish.attendance', + 'erxes.action.auto.distribute', + 'erxes.action.define.bot', + 'erxes.action.define.tags', + 'erxes.action.add.tags', + 'erxes.action.send.message', + 'erxes.action.send.template.message', + 'erxes.action.dispatch.to.app', + 'erxes.action.send.sms', + 'erxes.action.define.timeout', + 'erxes.action.send.internal.message', + 'erxes.action.execute.autmations.flow', + 'erxes.action.execute.action', + 'erxes.action.conditional', + 'erxes.action.wait.for.interaction', + 'erxes.action.define.workflow', + 'erxes.action.send.feedback', + 'erxes.action.add.comment.timeline', + 'erxes.action.choose.department', + 'erxes.action.to.ask', + 'erxes.action.send.request', + 'erxes.action.send.email', + 'erxes.action.send.file', + 'erxes.action.execute.javascript', + 'erxes.action.define.virtual.agent', + 'erxes.action.pause', + ].map(type => new FlowActionTypes({ type, createdAt: new Date() })); + + return FlowActionTypes.insertMany(flowActionTypes); + } + + return; + }) + + .then(async () => { + let flow = await Flows.createFlow({ + assignedUserId: 'wJjxQipSXXHbNyqFZ', + name: 'Teste', + description: 'Flow de teste', + }); + + const sendMessageType = await FlowActionTypes.findOne({ + type: 'erxes.action.send.message', + }); + const askType = await FlowActionTypes.findOne({ + type: 'erxes.action.to.ask', }); - console.log('\x1b[32m%s\x1b[0m', 'Your new password: ' + newPwd); + await FlowActions.createFlowAction({ + order: 0, + type: sendMessageType?.type, + flowId: flow.id, + actionId: sendMessageType?.id, + value: JSON.stringify({ + content: [ + 'Bem vindo, este é um teste de automação do WhatsBot, converse com o bot respondendo as perguntas que receber', + 'Olá, converse com o bot respondendo as perguntas que receber', + ], + }), + }); - return Users.findOne({ _id: user._id }); + await FlowActions.createFlowAction({ + order: 1, + type: askType?.type, + flowId: flow.id, + actionId: askType?.id, + value: JSON.stringify({ + content: [ + 'Com qual setor você gostaria de conversar? 1 - Administrativo 2 - Financeiro - 3 - Sobre', + 'Escolha abaixo o setor que melhor lhe atente? 1 - Administrativo 2 - Financeiro - 3 - Sobre', + ], + conditions: [ + { + operator: '=', + values: ['1', 'administrativo', 'adm'], + action: 'erxes.action.choose.department', + value: 'Administrativo', + }, + { + operator: '=', + values: ['2', 'financeiro'], + action: 'erxes.action.choose.department', + value: 'Financeiro', + }, + { + operator: '=', + values: ['3', 'sobre'], + action: 'erxes.action.execute.action', + value: '2', + }, + ], + }), + }); + + await FlowActions.createFlowAction({ + order: 2, + type: sendMessageType?.type, + flowId: flow.id, + actionId: sendMessageType?.id, + value: JSON.stringify({ + content: ['Este é um teste de automação do WhatsBot'], + }), + }); }) .then(() => { diff --git a/src/data/constants.ts b/src/data/constants.ts index 635386a63..ab107763e 100644 --- a/src/data/constants.ts +++ b/src/data/constants.ts @@ -278,4 +278,7 @@ export const MODULE_NAMES = { SEGMENT: 'segment', ENGAGE: 'engage', SCRIPT: 'script', + FLOW_ACTION_TYPE: 'flowActionType', + FLOW_ACTION: 'flowAction', + FLOW: 'flow', }; diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts new file mode 100644 index 000000000..c5c575996 --- /dev/null +++ b/src/data/modules/flow/index.ts @@ -0,0 +1,269 @@ +import * as strip from 'strip'; +import { + ConversationMessages, + Conversations, + Flows, + Integrations, + Users, + FlowActions, + Customers, +} from '../../../db/models'; + +import { IntegrationsAPI } from '../../dataSources'; + +import { IMessageDocument } from '../../../db/models/definitions/conversationMessages'; +import { + IFlowActionDocument, + IFlowActionValue, + IFlowActionValueCondition, +} from '../../../db/models/definitions/flowActions'; +import { IConversationDocument } from '../../../db/models/definitions/conversations'; +import { sendMessage } from '../../../messageBroker'; +import { KIND_CHOICES } from '../../../db/models/definitions/constants'; +import { IConversationMessageAdd } from '../../resolvers/mutations/conversations'; +import { graphqlPubsub } from '../../../pubsub'; +import Messages from '../../../db/models/ConversationMessages'; + +const handleCondition = (condition: IFlowActionValueCondition, content: string = '') => { + switch (condition.operator) { + case '=': + return condition.values.includes(content); + default: + return false; + } +}; + +const handleMessage = async (msg: IMessageDocument) => { + const conversation = await Conversations.getConversation(msg.conversationId); + + if (!conversation) return; + + const integration = await Integrations.getIntegration(conversation?.integrationId); + + if (!integration) return; + + let flowAction: IFlowActionDocument | null = null; + let sendNextMessage = false; + + if (conversation?.currentFlowActionId) { + flowAction = await FlowActions.getFlowAction(conversation.currentFlowActionId); + + if (flowAction) { + switch (flowAction.type) { + case 'erxes.action.send.message': + flowAction = await FlowActions.findOne({ + flowId: integration.flowId, + order: flowAction.order + 1, + }); + + if (flowAction?.type === 'erxes.action.send.message') sendNextMessage = true; + + break; + case 'erxes.action.to.ask': + const { conditions }: IFlowActionValue = JSON.parse(flowAction.value || '{}'); + + const condition = conditions.find(c => handleCondition(c, msg.content)); + + if (condition) { + switch (condition.action) { + case 'erxes.action.execute.action': + flowAction = await FlowActions.findOne({ + flowId: integration.flowId, + order: condition.value, + }); + break; + default: + break; + } + } + break; + default: + break; + } + } + } + + if (!flowAction) { + if (integration?.flowId) { + flowAction = await FlowActions.findOne({ + flowId: integration.flowId, + order: 0, + }); + + sendNextMessage = true; + } + } + + if (!flowAction) return; + + await Conversations.updateConversation(conversation.id, { + currentFlowActionId: flowAction.id, + }); + + switch (flowAction.type) { + case 'erxes.action.send.message': + await handleSendMessage(flowAction, conversation); + break; + + case 'erxes.action.to.ask': + await handleSendMessage(flowAction, conversation); + break; + default: + break; + } + + if (sendNextMessage) { + await handleMessage(msg); + } +}; + +const handleSendMessage = async (flowAction: IFlowActionDocument, conversation: IConversationDocument) => { + const integration = await Integrations.getIntegration(conversation.integrationId); + + if (!integration) return; + + const kind = integration.kind; + const integrationId = integration.id; + const conversationId = conversation.id; + + const customer = await Customers.findById(conversation.customerId); + + if (!customer) return; + + const flow = await Flows.findById(integration.flowId); + + if (!flow) return; + + const user = await Users.findById(flow?.assignedUserId); + + if (!user) return; + + const { content }: IFlowActionValue = JSON.parse(flowAction.value || '[]'); + + const length = content.length - 1; + const random = Math.random(); + const position = Math.round(length * random); + + const doc: IConversationMessageAdd = { + conversationId, + flowActionId: flowAction.id, + internal: false, + content: content[position], + }; + + const message = await ConversationMessages.addMessage(doc, user._id); + + let requestName; + let type; + let action; + + // send reply to facebook + if (kind === KIND_CHOICES.FACEBOOK_MESSENGER) { + type = 'facebook'; + action = 'reply-messenger'; + } + + // send reply to chatfuel + if (kind === KIND_CHOICES.CHATFUEL) { + requestName = 'replyChatfuel'; + } + + if (kind === KIND_CHOICES.TWITTER_DM) { + requestName = 'replyTwitterDm'; + } + + if (kind === KIND_CHOICES.WHATSAPP) { + requestName = 'replyWhatsapp'; + } + + if (kind === KIND_CHOICES.WHATSPRO) { + type = 'whatspro'; + requestName = 'replyWhatsPro'; + } + + await sendConversationToIntegrations( + type, + integrationId, + conversationId, + requestName, + doc, + { IntegrationsAPI: new IntegrationsAPI() }, + action, + ); + + const dbMessage = await ConversationMessages.getMessage(message._id); + + // Publishing both admin & client + publishMessage(dbMessage, conversation.customerId); +}; + +/** + * Send conversation to integrations + */ + +const sendConversationToIntegrations = ( + type: string, + integrationId: string, + conversationId: string, + requestName: string, + doc: IConversationMessageAdd, + dataSources: any, + action?: string, +) => { + if (type === 'facebook') { + return sendMessage('erxes-api:integrations-notification', { + action, + type, + payload: JSON.stringify({ + integrationId, + conversationId, + content: strip(doc.content), + attachments: doc.attachments || [], + }), + }); + } + + if (type === 'whatspro') { + doc.content = doc.content.replace(/<\/?(b|strong)>/g, '*'); + doc.content = doc.content.replace(/
/g, '\n'); + doc.content = doc.content.replace(/<\/?i>/g, '_'); + doc.content = doc.content.replace(/<\/?s>/g, '~'); + + doc.content += '\n\n```WhatsPro Bot```'; + } + + if (dataSources && dataSources.IntegrationsAPI && requestName) { + return dataSources.IntegrationsAPI[requestName]({ + conversationId, + integrationId, + content: strip(doc.content), + attachments: doc.attachments || [], + }); + } +}; + +/** + * Publish admin's message + */ +export const publishMessage = async (message: IMessageDocument, customerId?: string) => { + graphqlPubsub.publish('conversationMessageInserted', { + conversationMessageInserted: message, + }); + + // widget is listening for this subscription to show notification + // customerId available means trying to notify to client + if (customerId) { + const unreadCount = await Messages.widgetsGetUnreadMessagesCount(message.conversationId); + + graphqlPubsub.publish('conversationAdminMessageInserted', { + conversationAdminMessageInserted: { + customerId, + unreadCount, + }, + }); + } +}; + +export default { + handleMessage, +}; diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index eb7ee11dc..8fca7709b 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -2,6 +2,7 @@ import { ConversationMessages, Conversations, Customers, Integrations, Users } f import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; import { graphqlPubsub } from '../../../pubsub'; import { getConfigs } from '../../utils'; +import flow from '../flow'; const sendError = message => ({ status: 'error', @@ -111,6 +112,8 @@ export const receiveRpcMessage = async msg => { conversationMessageInserted: message, }); + flow.handleMessage(message); + return sendSuccess({ _id: message._id }); } diff --git a/src/data/resolvers/flowActionTypes.ts b/src/data/resolvers/flowActionTypes.ts new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/src/data/resolvers/flowActionTypes.ts @@ -0,0 +1 @@ +export default {}; diff --git a/src/data/resolvers/flowActions.ts b/src/data/resolvers/flowActions.ts new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/src/data/resolvers/flowActions.ts @@ -0,0 +1 @@ +export default {}; diff --git a/src/data/resolvers/flows.ts b/src/data/resolvers/flows.ts new file mode 100644 index 000000000..7827c54ec --- /dev/null +++ b/src/data/resolvers/flows.ts @@ -0,0 +1,8 @@ +import { Integrations } from '../../db/models'; +import { IFlowDocument } from '../../db/models/definitions/flows'; + +export default { + integrations(flow: IFlowDocument) { + return Integrations.findIntegrations({ flowId: flow._id }); + }, +}; diff --git a/src/data/resolvers/index.ts b/src/data/resolvers/index.ts index d559cebd5..1631c8446 100644 --- a/src/data/resolvers/index.ts +++ b/src/data/resolvers/index.ts @@ -37,6 +37,9 @@ import Task from './tasks'; import Ticket from './tickets'; import User from './user'; import UsersGroup from './usersGroup'; +import FlowActionType from './flowActionTypes'; +import FlowAction from './flowActions'; +import Flow from './flows'; const resolvers: any = { ...customScalars, @@ -85,6 +88,10 @@ const resolvers: any = { UsersGroup, Pipeline, GrowthHack, + + FlowActionType, + FlowAction, + Flow, }; export default resolvers; diff --git a/src/data/resolvers/mutations/conversations.ts b/src/data/resolvers/mutations/conversations.ts index 368471753..f064bdeaf 100644 --- a/src/data/resolvers/mutations/conversations.ts +++ b/src/data/resolvers/mutations/conversations.ts @@ -26,6 +26,7 @@ export interface IConversationMessageAdd { mentionedUserIds?: string[]; internal?: boolean; attachments?: any; + flowActionId?: string; } interface IReplyFacebookComment { diff --git a/src/data/resolvers/mutations/flowActionTypes.ts b/src/data/resolvers/mutations/flowActionTypes.ts new file mode 100644 index 000000000..a6a79bb05 --- /dev/null +++ b/src/data/resolvers/mutations/flowActionTypes.ts @@ -0,0 +1,65 @@ +import { FlowActionTypes } from '../../../db/models'; +import { IFlowActionType } from '../../../db/models/definitions/flowActionTypes'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; +import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +interface IFlowActionTypesEdit extends IFlowActionType { + _id: string; +} + +const flowActionTypeMutations = { + /** + * Create new flowActionType + */ + async flowActionTypesAdd(_root, doc: IFlowActionType, { user }: IContext) { + const flowActionType = await FlowActionTypes.createFlowActionType({ ...doc }); + + await putCreateLog( + { + type: MODULE_NAMES.FLOW_ACTION_TYPE, + newData: { ...doc, userId: user._id }, + object: flowActionType, + }, + user, + ); + + return flowActionType; + }, + + /** + * Update flowActionType + */ + async flowActionTypesEdit(_root, { _id, ...fields }: IFlowActionTypesEdit, { user }: IContext) { + const flowActionType = await FlowActionTypes.getFlowActionType(_id); + const updated = await FlowActionTypes.updateFlowActionType(_id, fields); + + await putUpdateLog( + { + type: MODULE_NAMES.FLOW_ACTION_TYPE, + object: flowActionType, + newData: fields, + }, + user, + ); + + return updated; + }, + + /** + * Delete flowActionType + */ + async flowActionTypesRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const flowActionType = await FlowActionTypes.getFlowActionType(_id); + const removed = await FlowActionTypes.removeFlowActionType(_id); + + await putDeleteLog({ type: MODULE_NAMES.FLOW_ACTION_TYPE, object: flowActionType }, user); + + return removed; + }, +}; + +moduleCheckPermission(flowActionTypeMutations, 'manageFlowActionTypes'); + +export default flowActionTypeMutations; diff --git a/src/data/resolvers/mutations/flowActions.ts b/src/data/resolvers/mutations/flowActions.ts new file mode 100644 index 000000000..c352d2fe1 --- /dev/null +++ b/src/data/resolvers/mutations/flowActions.ts @@ -0,0 +1,65 @@ +import { FlowActions } from '../../../db/models'; +import { IFlowAction } from '../../../db/models/definitions/flowActions'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; +import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +interface IFlowActionsEdit extends IFlowAction { + _id: string; +} + +const flowActionMutations = { + /** + * Create new flowAction + */ + async flowActionsAdd(_root, doc: IFlowAction, { user }: IContext) { + const flowAction = await FlowActions.createFlowAction({ ...doc }); + + await putCreateLog( + { + type: MODULE_NAMES.FLOW_ACTION, + newData: { ...doc, userId: user._id }, + object: flowAction, + }, + user, + ); + + return flowAction; + }, + + /** + * Update flowAction + */ + async flowActionsEdit(_root, { _id, ...fields }: IFlowActionsEdit, { user }: IContext) { + const flowAction = await FlowActions.getFlowAction(_id); + const updated = await FlowActions.updateFlowAction(_id, fields); + + await putUpdateLog( + { + type: MODULE_NAMES.FLOW_ACTION, + object: flowAction, + newData: fields, + }, + user, + ); + + return updated; + }, + + /** + * Delete flowAction + */ + async flowActionsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const flowAction = await FlowActions.getFlowAction(_id); + const removed = await FlowActions.removeFlowAction(_id); + + await putDeleteLog({ type: MODULE_NAMES.FLOW_ACTION, object: flowAction }, user); + + return removed; + }, +}; + +moduleCheckPermission(flowActionMutations, 'manageFlowActions'); + +export default flowActionMutations; diff --git a/src/data/resolvers/mutations/flows.ts b/src/data/resolvers/mutations/flows.ts new file mode 100644 index 000000000..b26f79ecc --- /dev/null +++ b/src/data/resolvers/mutations/flows.ts @@ -0,0 +1,71 @@ +import { Flows } from '../../../db/models'; +import { IFlow } from '../../../db/models/definitions/flows'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; +import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +interface IFlowsEdit extends IFlow { + _id: string; +} + +const flowMutations = { + /** + * Create new flow + */ + async flowsAdd(_root, doc: IFlow, { user }: IContext) { + const flow = await Flows.createFlow({ ...doc }); + + await putCreateLog( + { + type: MODULE_NAMES.FLOW, + newData: { ...doc, userId: user._id }, + object: flow, + }, + user, + ); + + return flow; + }, + + /** + * Update flow + */ + async flowsEdit(_root, { _id, ...fields }: IFlowsEdit, { user }: IContext) { + const flow = await Flows.getFlow(_id); + const updated = await Flows.updateFlow(_id, fields); + + await putUpdateLog( + { + type: MODULE_NAMES.FLOW, + object: flow, + newData: fields, + }, + user, + ); + + return updated; + }, + + /** + * Delete flow + */ + async flowsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const flow = await Flows.getFlow(_id); + const removed = await Flows.removeFlow(_id); + + await putDeleteLog({ type: MODULE_NAMES.FLOW, object: flow }, user); + + return removed; + }, + /** + * Update flowId fields in given Integrations + */ + async flowsManageIntegrations(_root, { _id, integrationIds }: { _id: string; integrationIds: string[] }) { + return Flows.manageIntegrations({ _id, integrationIds }); + }, +}; + +moduleCheckPermission(flowMutations, 'manageFlows'); + +export default flowMutations; diff --git a/src/data/resolvers/mutations/index.ts b/src/data/resolvers/mutations/index.ts index f0f808034..f8698870b 100644 --- a/src/data/resolvers/mutations/index.ts +++ b/src/data/resolvers/mutations/index.ts @@ -32,6 +32,9 @@ import tasks from './tasks'; import tickets from './tickets'; import users from './users'; import widgets from './widgets'; +import flowActionTypes from './flowActionTypes'; +import flowActions from './flowActions'; +import flows from './flows'; export default { ...users, @@ -70,4 +73,7 @@ export default { ...checklists, ...robot, ...widgets, + ...flowActionTypes, + ...flowActions, + ...flows, }; diff --git a/src/data/resolvers/queries/flowActionTypes.ts b/src/data/resolvers/queries/flowActionTypes.ts new file mode 100644 index 000000000..058a7a12d --- /dev/null +++ b/src/data/resolvers/queries/flowActionTypes.ts @@ -0,0 +1,61 @@ +import { FlowActionTypes } from '../../../db/models'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +interface IListArgs { + page?: number; + perPage?: number; + searchValue?: string; +} + +const queryBuilder = (params: IListArgs, flowActionTypeIdSelector: any) => { + const selector: any = { ...flowActionTypeIdSelector }; + + const { searchValue } = params; + + if (searchValue) { + selector.name = new RegExp(`.*${params.searchValue}.*`, 'i'); + } + + return selector; +}; + +const flowActionTypeQueries = { + /** + * FlowActionTypes list + */ + flowActionTypes(_root, args: IListArgs, { flowActionTypeIdSelector }: IContext) { + const selector = queryBuilder(args, flowActionTypeIdSelector); + + return FlowActionTypes.find(selector).sort({ createdAt: -1 }); + }, + + /** + * Get one flowActionType + */ + flowActionTypeDetail(_root, { _id }: { _id: string }) { + return FlowActionTypes.findOne({ _id }); + }, + + /** + * Get all flowActionTypes count. We will use it in pager + */ + flowActionTypesTotalCount(_root, _args, { flowActionTypeIdSelector }: IContext) { + return FlowActionTypes.find(flowActionTypeIdSelector).countDocuments(); + }, + + /** + * Get last flowActionType + */ + flowActionTypesGetLast() { + return FlowActionTypes.findOne({}).sort({ createdAt: -1 }); + }, +}; + +requireLogin(flowActionTypeQueries, 'flowActionTypesTotalCount'); +requireLogin(flowActionTypeQueries, 'flowActionTypesGetLast'); +requireLogin(flowActionTypeQueries, 'flowActionTypeDetail'); + +checkPermission(flowActionTypeQueries, 'flowActionTypes', 'showFlowActionTypes', []); + +export default flowActionTypeQueries; diff --git a/src/data/resolvers/queries/flowActions.ts b/src/data/resolvers/queries/flowActions.ts new file mode 100644 index 000000000..27927a89b --- /dev/null +++ b/src/data/resolvers/queries/flowActions.ts @@ -0,0 +1,61 @@ +import { FlowActions } from '../../../db/models'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +interface IListArgs { + page?: number; + perPage?: number; + searchValue?: string; +} + +const queryBuilder = (params: IListArgs, flowActionIdSelector: any) => { + const selector: any = { ...flowActionIdSelector }; + + const { searchValue } = params; + + if (searchValue) { + selector.name = new RegExp(`.*${params.searchValue}.*`, 'i'); + } + + return selector; +}; + +const flowActionQueries = { + /** + * FlowActions list + */ + flowActions(_root, args: IListArgs, { flowActionIdSelector }: IContext) { + const selector = queryBuilder(args, flowActionIdSelector); + + return FlowActions.find(selector).sort({ createdAt: -1 }); + }, + + /** + * Get one flowAction + */ + flowActionDetail(_root, { _id }: { _id: string }) { + return FlowActions.findOne({ _id }); + }, + + /** + * Get all flowActions count. We will use it in pager + */ + flowActionsTotalCount(_root, _args, { flowActionIdSelector }: IContext) { + return FlowActions.find(flowActionIdSelector).countDocuments(); + }, + + /** + * Get last flowAction + */ + flowActionsGetLast() { + return FlowActions.findOne({}).sort({ createdAt: -1 }); + }, +}; + +requireLogin(flowActionQueries, 'flowActionsTotalCount'); +requireLogin(flowActionQueries, 'flowActionsGetLast'); +requireLogin(flowActionQueries, 'flowActionDetail'); + +checkPermission(flowActionQueries, 'flowActions', 'showFlowActions', []); + +export default flowActionQueries; diff --git a/src/data/resolvers/queries/flows.ts b/src/data/resolvers/queries/flows.ts new file mode 100644 index 000000000..405c1b86d --- /dev/null +++ b/src/data/resolvers/queries/flows.ts @@ -0,0 +1,61 @@ +import { Flows } from '../../../db/models'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +interface IListArgs { + page?: number; + perPage?: number; + searchValue?: string; +} + +const queryBuilder = (params: IListArgs, flowIdSelector: any) => { + const selector: any = { ...flowIdSelector }; + + const { searchValue } = params; + + if (searchValue) { + selector.name = new RegExp(`.*${params.searchValue}.*`, 'i'); + } + + return selector; +}; + +const flowQueries = { + /** + * Flows list + */ + flows(_root, args: IListArgs, { flowIdSelector }: IContext) { + const selector = queryBuilder(args, flowIdSelector); + + return Flows.find(selector).sort({ createdAt: -1 }); + }, + + /** + * Get one flow + */ + flowDetail(_root, { _id }: { _id: string }) { + return Flows.findOne({ _id }); + }, + + /** + * Get all flows count. We will use it in pager + */ + flowsTotalCount(_root, _args, { flowIdSelector }: IContext) { + return Flows.find(flowIdSelector).countDocuments(); + }, + + /** + * Get last flow + */ + flowsGetLast() { + return Flows.findOne({}).sort({ createdAt: -1 }); + }, +}; + +requireLogin(flowQueries, 'flowsTotalCount'); +requireLogin(flowQueries, 'flowsGetLast'); +requireLogin(flowQueries, 'flowDetail'); + +checkPermission(flowQueries, 'flows', 'showFlows', []); + +export default flowQueries; diff --git a/src/data/resolvers/queries/index.ts b/src/data/resolvers/queries/index.ts index e49ba75dd..ce32699d3 100644 --- a/src/data/resolvers/queries/index.ts +++ b/src/data/resolvers/queries/index.ts @@ -36,6 +36,9 @@ import tasks from './tasks'; import tickets from './tickets'; import users from './users'; import widgets from './widgets'; +import flowActionTypes from './flowActionTypes'; +import flowActions from './flowActions'; +import flows from './flows'; export default { ...users, @@ -78,4 +81,7 @@ export default { ...robot, ...pipelineLabels, ...widgets, + ...flowActionTypes, + ...flowActions, + ...flows, }; diff --git a/src/data/resolvers/queries/logs.ts b/src/data/resolvers/queries/logs.ts index fedefa8c0..7e3a2cbc5 100644 --- a/src/data/resolvers/queries/logs.ts +++ b/src/data/resolvers/queries/logs.ts @@ -37,6 +37,9 @@ import { conditionSchema, segmentSchema } from '../../../db/models/definitions/s import { tagSchema } from '../../../db/models/definitions/tags'; import { taskSchema } from '../../../db/models/definitions/tasks'; import { ticketSchema } from '../../../db/models/definitions/tickets'; +import { flowActionTypeSchema } from '../../../db/models/definitions/flowActionTypes'; +import { flowActionSchema } from '../../../db/models/definitions/flowActions'; +import { flowSchema } from '../../../db/models/definitions/flows'; import { MODULE_NAMES } from '../../constants'; import { fetchLogs, ILogQueryParams } from '../../logUtils'; import { checkPermission } from '../../permissions/wrappers'; @@ -188,6 +191,18 @@ const LOG_MAPPINGS: ISchemaMap[] = [ name: MODULE_NAMES.SCRIPT, schemas: [scriptSchema], }, + { + name: MODULE_NAMES.FLOW_ACTION_TYPE, + schemas: [flowActionTypeSchema], + }, + { + name: MODULE_NAMES.FLOW_ACTION, + schemas: [flowActionSchema], + }, + { + name: MODULE_NAMES.FLOW, + schemas: [flowSchema], + }, ]; /** diff --git a/src/data/schema/conversation.ts b/src/data/schema/conversation.ts index b01d186fb..c617f470b 100644 --- a/src/data/schema/conversation.ts +++ b/src/data/schema/conversation.ts @@ -36,6 +36,8 @@ export const types = ` participatedUsers: [User] participatorCount: Int videoCallData: VideoCallData + + currentFlowActionId: String } type EngageData { diff --git a/src/data/schema/flow.ts b/src/data/schema/flow.ts new file mode 100644 index 000000000..10162d03e --- /dev/null +++ b/src/data/schema/flow.ts @@ -0,0 +1,25 @@ +export const types = ` + type Flow { + _id: String! + name: String + description: String + actions: [FlowAction] + integrations: [Integration] + assignedUserId: String + createdAt: Date + } +`; + +export const queries = ` + flows(page: Int, perPage: Int, searchValue: String): [Flow] + flowDetail(_id: String!): Flow + flowsTotalCount: Int + flowsGetLast: Flow +`; + +export const mutations = ` + flowsAdd(name: String!, description: String): Flow + flowsEdit(_id: String!, name: String!, description: String): Flow + flowsRemove(_id: String!): JSON + flowsManageIntegrations(_id: String!, integrationIds: [String]!): [Integration] +`; diff --git a/src/data/schema/flowAction.ts b/src/data/schema/flowAction.ts new file mode 100644 index 000000000..fdfc47796 --- /dev/null +++ b/src/data/schema/flowAction.ts @@ -0,0 +1,24 @@ +export const types = ` + type FlowAction { + _id: String! + actionId: String! + type: String! + value: String! + order: Int! + flowId: String! + createdAt: Date + } +`; + +export const queries = ` + flowActions(page: Int, perPage: Int, searchValue: String): [FlowAction] + flowActionDetail(_id: String!): FlowAction + flowActionsTotalCount: Int + flowActionsGetLast: FlowAction +`; + +export const mutations = ` + flowActionsAdd(name: String!, description: String): FlowAction + flowActionsEdit(_id: String!, name: String!, description: String): FlowAction + flowActionsRemove(_id: String!): JSON +`; diff --git a/src/data/schema/flowActionType.ts b/src/data/schema/flowActionType.ts new file mode 100644 index 000000000..720da9d4f --- /dev/null +++ b/src/data/schema/flowActionType.ts @@ -0,0 +1,22 @@ +export const types = ` + type FlowActionType { + _id: String! + type: String! + name: String + description: String + createdAt: Date + } +`; + +export const queries = ` + flowActionTypes(page: Int, perPage: Int, searchValue: String): [FlowActionType] + flowActionTypeDetail(_id: String!): FlowActionType + flowActionTypesTotalCount: Int + flowActionTypesGetLast: FlowActionType +`; + +export const mutations = ` + flowActionTypesAdd(name: String!, description: String): FlowActionType + flowActionTypesEdit(_id: String!, name: String!, description: String): FlowActionType + flowActionTypesRemove(_id: String!): JSON +`; diff --git a/src/data/schema/index.ts b/src/data/schema/index.ts index 28190a37a..00ea8f2d1 100755 --- a/src/data/schema/index.ts +++ b/src/data/schema/index.ts @@ -128,6 +128,14 @@ import { import { mutations as WidgetMutations, queries as WidgetQueries, types as WidgetTypes } from './widget'; +import { + mutations as FlowActionTypeMutations, + queries as FlowActionTypeQueries, + types as FlowActionTypeTypes, +} from './flowActionType'; +import { mutations as FlowActionMutations, queries as FlowActionQueries, types as FlowActionTypes } from './flowAction'; +import { mutations as FlowMutations, queries as FlowQueries, types as FlowTypes } from './flow'; + export const types = ` scalar JSON scalar Date @@ -171,6 +179,9 @@ export const types = ` ${RobotTypes} ${PipelineLabelTypes} ${WidgetTypes} + ${FlowActionTypeTypes} + ${FlowActionTypes} + ${FlowTypes} `; export const queries = ` @@ -213,6 +224,9 @@ export const queries = ` ${RobotQueries} ${PipelineLabelQueries} ${WidgetQueries} + ${FlowActionTypeQueries} + ${FlowActionQueries} + ${FlowQueries} } `; @@ -253,6 +267,9 @@ export const mutations = ` ${RobotMutations} ${PipelineLabelMutations} ${WidgetMutations} + ${FlowActionTypeMutations} + ${FlowActionMutations} + ${FlowMutations} } `; diff --git a/src/data/schema/integration.ts b/src/data/schema/integration.ts index d9e00f543..24c84c079 100644 --- a/src/data/schema/integration.ts +++ b/src/data/schema/integration.ts @@ -17,6 +17,8 @@ export const types = ` brand: Brand form: Form channels: [Channel] + + flowId: String } type integrationsTotalCount { @@ -93,7 +95,8 @@ export const queries = ` searchValue: String, channelId: String, brandId: String, - tag: String + tag: String, + flowId: String ): [Integration] integrationsGetUsedTypes: [integrationsGetUsedTypes] diff --git a/src/data/types.ts b/src/data/types.ts index 5348c0396..501d7afd8 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -7,6 +7,9 @@ export interface IContext { user: IUserDocument; docModifier: (doc: T) => any; brandIdSelector: {}; + flowActionTypeIdSelector: {}; + flowActionIdSelector: {}; + flowIdSelector: {}; userBrandIdsSelector: {}; commonQuerySelector: {}; commonQuerySelectorElk: {}; diff --git a/src/db/factories.ts b/src/db/factories.ts index c4b45d763..e5cb08dab 100644 --- a/src/db/factories.ts +++ b/src/db/factories.ts @@ -47,6 +47,9 @@ import { Tickets, Users, UsersGroups, + FlowActionTypes, + FlowActions, + Flows, } from './models'; import { ACTIVITY_CONTENT_TYPES, @@ -1329,3 +1332,51 @@ export function engageDataFactory(params: IMessageEngageDataParams) { sentAs: params.sentAs || 'post', }; } + +interface IFlowActionTypeFactoryInput { + type?: string; + name?: string; + description?: string; +} + +export const flowActionTypeFactory = async (params: IFlowActionTypeFactoryInput = {}) => { + const flowActionType = new FlowActionTypes({ + name: params.name || faker.random.word(), + type: params.type, + description: params.description || faker.random.word(), + createdAt: new Date(), + }); + return flowActionType.save(); +}; + +interface IFlowActionFactoryInput { + type?: string; + name?: string; + description?: string; +} + +export const flowActionFactory = async (params: IFlowActionFactoryInput = {}) => { + const flowAction = new FlowActions({ + name: params.name || faker.random.word(), + type: params.type, + description: params.description || faker.random.word(), + createdAt: new Date(), + }); + return flowAction.save(); +}; + +interface IFlowFactoryInput { + type?: string; + name?: string; + description?: string; +} + +export const flowFactory = async (params: IFlowFactoryInput = {}) => { + const flow = new Flows({ + name: params.name || faker.random.word(), + type: params.type, + description: params.description || faker.random.word(), + createdAt: new Date(), + }); + return flow.save(); +}; diff --git a/src/db/models/FlowActionTypes.ts b/src/db/models/FlowActionTypes.ts new file mode 100644 index 000000000..0f5c5d416 --- /dev/null +++ b/src/db/models/FlowActionTypes.ts @@ -0,0 +1,59 @@ +import { Model, model } from 'mongoose'; +import { flowActionTypeSchema, IFlowActionType, IFlowActionTypeDocument } from './definitions/flowActionTypes'; + +export interface IFlowActionTypeModel extends Model { + getFlowActionType(_id: string): IFlowActionTypeDocument; + createFlowActionType(doc: IFlowActionType): IFlowActionTypeDocument; + updateFlowActionType(_id: string, fields: IFlowActionType): IFlowActionTypeDocument; + removeFlowActionType(_id: string): IFlowActionTypeDocument; +} + +export const loadClass = () => { + class FlowActionType { + /* + * Get a FlowActionType + */ + public static async getFlowActionType(_id: string) { + const flowActionType = await FlowActionTypes.findOne({ _id }); + + if (!flowActionType) { + throw new Error('FlowActionType not found'); + } + + return flowActionType; + } + + public static async createFlowActionType(doc: IFlowActionType) { + // generate code automatically + // if there is no flowActionType code defined + return FlowActionTypes.create({ + ...doc, + createdAt: new Date(), + }); + } + + public static async updateFlowActionType(_id: string, fields: IFlowActionType) { + await FlowActionTypes.updateOne({ _id }, { $set: { ...fields } }); + return FlowActionTypes.findOne({ _id }); + } + + public static async removeFlowActionType(_id) { + const flowActionTypeObj = await FlowActionTypes.findOne({ _id }); + + if (!flowActionTypeObj) { + throw new Error(`FlowActionType not found with id ${_id}`); + } + + return flowActionTypeObj.remove(); + } + } + + flowActionTypeSchema.loadClass(FlowActionType); +}; + +loadClass(); + +// tslint:disable-next-line +const FlowActionTypes = model('flow_action_types', flowActionTypeSchema); + +export default FlowActionTypes; diff --git a/src/db/models/FlowActions.ts b/src/db/models/FlowActions.ts new file mode 100644 index 000000000..522d4e445 --- /dev/null +++ b/src/db/models/FlowActions.ts @@ -0,0 +1,59 @@ +import { Model, model } from 'mongoose'; +import { flowActionSchema, IFlowAction, IFlowActionDocument } from './definitions/flowActions'; + +export interface IFlowActionModel extends Model { + getFlowAction(_id: string): IFlowActionDocument; + createFlowAction(doc: IFlowAction): IFlowActionDocument; + updateFlowAction(_id: string, fields: IFlowAction): IFlowActionDocument; + removeFlowAction(_id: string): IFlowActionDocument; +} + +export const loadClass = () => { + class FlowAction { + /* + * Get a FlowAction + */ + public static async getFlowAction(_id: string) { + const flowAction = await FlowActions.findOne({ _id }); + + if (!flowAction) { + throw new Error('FlowAction not found'); + } + + return flowAction; + } + + public static async createFlowAction(doc: IFlowAction) { + // generate code automatically + // if there is no flowAction code defined + return FlowActions.create({ + ...doc, + createdAt: new Date(), + }); + } + + public static async updateFlowAction(_id: string, fields: IFlowAction) { + await FlowActions.updateOne({ _id }, { $set: { ...fields } }); + return FlowActions.findOne({ _id }); + } + + public static async removeFlowAction(_id) { + const flowActionObj = await FlowActions.findOne({ _id }); + + if (!flowActionObj) { + throw new Error(`FlowAction not found with id ${_id}`); + } + + return flowActionObj.remove(); + } + } + + flowActionSchema.loadClass(FlowAction); +}; + +loadClass(); + +// tslint:disable-next-line +const FlowActions = model('flow_actions', flowActionSchema); + +export default FlowActions; diff --git a/src/db/models/Flows.ts b/src/db/models/Flows.ts new file mode 100644 index 000000000..a2f0470ff --- /dev/null +++ b/src/db/models/Flows.ts @@ -0,0 +1,69 @@ +import { Model, model } from 'mongoose'; +import { Integrations } from './'; +import { flowSchema, IFlow, IFlowDocument } from './definitions/flows'; +import { IIntegrationDocument } from './definitions/integrations'; + +export interface IFlowModel extends Model { + getFlow(_id: string): IFlowDocument; + createFlow(doc: IFlow): IFlowDocument; + updateFlow(_id: string, fields: IFlow): IFlowDocument; + removeFlow(_id: string): IFlowDocument; + + manageIntegrations({ _id, integrationIds }: { _id: string; integrationIds: string[] }): IIntegrationDocument[]; +} + +export const loadClass = () => { + class Flow { + /* + * Get a Flow + */ + public static async getFlow(_id: string) { + const flow = await Flows.findOne({ _id }); + + if (!flow) { + throw new Error('Flow not found'); + } + + return flow; + } + + public static async createFlow(doc: IFlow) { + // generate code automatically + // if there is no flow code defined + return Flows.create({ + ...doc, + createdAt: new Date(), + }); + } + + public static async updateFlow(_id: string, fields: IFlow) { + await Flows.updateOne({ _id }, { $set: { ...fields } }); + return Flows.findOne({ _id }); + } + + public static async removeFlow(_id) { + const flowObj = await Flows.findOne({ _id }); + + if (!flowObj) { + throw new Error(`Flow not found with id ${_id}`); + } + + return flowObj.remove(); + } + + public static async manageIntegrations({ _id, integrationIds }: { _id: string; integrationIds: string[] }) { + await Integrations.updateMany({ _id: { $in: integrationIds } }, { $set: { flowId: _id } }, { multi: true }); + + return Integrations.findIntegrations({ _id: { $in: integrationIds } }); + } + } + + flowSchema.loadClass(Flow); +}; + +loadClass(); + +// tslint:disable-next-line +const Flows = model('flows', flowSchema); + +export default Flows; diff --git a/src/db/models/definitions/conversationMessages.ts b/src/db/models/definitions/conversationMessages.ts index 67254adc6..4180edce0 100644 --- a/src/db/models/definitions/conversationMessages.ts +++ b/src/db/models/definitions/conversationMessages.ts @@ -40,6 +40,7 @@ export interface IMessage { messengerAppData?: any; engageData?: IEngageData; contentType?: string; + flowActionId?: string; } export interface IMessageDocument extends IMessage, Document { @@ -101,4 +102,5 @@ export const messageSchema = new Schema({ enum: MESSAGE_TYPES.ALL, default: MESSAGE_TYPES.TEXT, }), + flowActionId: field({ type: String, index: true }), }); diff --git a/src/db/models/definitions/conversations.ts b/src/db/models/definitions/conversations.ts index e834ab553..59d57eb36 100644 --- a/src/db/models/definitions/conversations.ts +++ b/src/db/models/definitions/conversations.ts @@ -25,6 +25,8 @@ export interface IConversation { firstRespondedUserId?: string; firstRespondedDate?: Date; + + currentFlowActionId?: string; } // Conversation schema @@ -70,4 +72,6 @@ export const conversationSchema = new Schema({ firstRespondedUserId: field({ type: String }), firstRespondedDate: field({ type: Date }), + + currentFlowActionId: field({ type: String }), }); diff --git a/src/db/models/definitions/flowActionTypes.ts b/src/db/models/definitions/flowActionTypes.ts new file mode 100644 index 000000000..1111ba37a --- /dev/null +++ b/src/db/models/definitions/flowActionTypes.ts @@ -0,0 +1,23 @@ +import { Document, Schema } from 'mongoose'; +import { field } from './utils'; + +export interface IFlowActionType { + type?: string; + name?: string; + description?: string; +} + +export interface IFlowActionTypeDocument extends IFlowActionType, Document { + _id: string; + createdAt: Date; +} + +// Mongoose schemas =========== + +export const flowActionTypeSchema = new Schema({ + _id: field({ pkey: true }), + type: field({ type: String, label: 'Type' }), + name: field({ type: String, label: 'Name' }), + description: field({ type: String, optional: true, label: 'Description' }), + createdAt: field({ type: Date, label: 'Created at' }), +}); diff --git a/src/db/models/definitions/flowActions.ts b/src/db/models/definitions/flowActions.ts new file mode 100644 index 000000000..894a8d896 --- /dev/null +++ b/src/db/models/definitions/flowActions.ts @@ -0,0 +1,44 @@ +import { Document, Schema } from 'mongoose'; +import { field } from './utils'; + +export interface IFlowAction { + type?: string; + name?: string; + description?: string; + value?: string; + order: number; + flowId?: string; + actionId?: string; +} + +export interface IFlowActionDocument extends IFlowAction, Document { + _id: string; + createdAt: Date; +} + +export interface IFlowActionValueCondition { + operator: string; + values: string[]; + action: string; + value: string; +} + +export interface IFlowActionValue { + content: string[]; + conditions: IFlowActionValueCondition[]; +} + +// Mongoose schemas =========== + +export const flowActionSchema = new Schema({ + _id: field({ pkey: true }), + type: field({ type: String, label: 'Type' }), + name: field({ type: String, label: 'Name' }), + description: field({ type: String, optional: true, label: 'Description' }), + flowId: field({ type: String, label: 'Flow' }), + actionId: field({ type: String, label: 'Flow Action Type' }), + userId: field({ type: String, label: 'Created by' }), + value: field({ type: String, label: 'Value' }), + order: field({ type: Number, label: 'Order' }), + createdAt: field({ type: Date, label: 'Created at' }), +}); diff --git a/src/db/models/definitions/flows.ts b/src/db/models/definitions/flows.ts new file mode 100644 index 000000000..2522d801b --- /dev/null +++ b/src/db/models/definitions/flows.ts @@ -0,0 +1,24 @@ +import { Document, Schema } from 'mongoose'; +import { field } from './utils'; + +export interface IFlow { + name?: string; + description?: string; + assignedUserId?: string; +} + +export interface IFlowDocument extends IFlow, Document { + _id: string; + createdAt: Date; +} + +// Mongoose schemas =========== + +export const flowSchema = new Schema({ + _id: field({ pkey: true }), + name: field({ type: String, label: 'Name' }), + description: field({ type: String, optional: true, label: 'Description' }), + userId: field({ type: String, label: 'Created by' }), + assignedUserId: field({ type: String }), + createdAt: field({ type: Date, label: 'Created at' }), +}); diff --git a/src/db/models/definitions/integrations.ts b/src/db/models/definitions/integrations.ts index 0eaa54250..82a88e225 100644 --- a/src/db/models/definitions/integrations.ts +++ b/src/db/models/definitions/integrations.ts @@ -99,6 +99,7 @@ export interface IIntegration { messengerData?: IMessengerData; uiOptions?: IUiOptions; isActive?: boolean; + flowId?: string; } export interface IIntegrationDocument extends IIntegration, Document { @@ -290,6 +291,7 @@ export const integrationSchema = new Schema({ formId: field({ type: String, label: 'Form' }), leadData: field({ type: leadDataSchema, label: 'Lead data' }), isActive: field({ type: Boolean, optional: true, default: true, label: 'Is active' }), + flowId: field({ type: String, label: 'Flow' }), // TODO: remove formData: field({ type: leadDataSchema }), messengerData: field({ type: messengerDataSchema }), diff --git a/src/db/models/index.ts b/src/db/models/index.ts index 426355006..451bc38f9 100644 --- a/src/db/models/index.ts +++ b/src/db/models/index.ts @@ -34,6 +34,9 @@ import Tags from './Tags'; import Tasks from './Tasks'; import Tickets from './Tickets'; import Users from './Users'; +import FlowActionTypes from './FlowActionTypes'; +import FlowActions from './FlowActions'; +import Flows from './Flows'; export { EmailDeliveries, @@ -82,4 +85,7 @@ export { PipelineLabels, Checklists, ChecklistItems, + FlowActionTypes, + FlowActions, + Flows, }; From 6d2d5a1313059dc557361ec03f5b688f654dcc27 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 20 Mar 2020 13:53:10 -0300 Subject: [PATCH 03/71] fix --- .vscode/launch.json | 22 ++++++++++++++++++++++ package.json | 1 + src/data/modules/flow/index.ts | 4 ---- 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..30617f745 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}\\dist\\index.js", + "preLaunchTask": "tsc: build - tsconfig.json", + "envFile": "${workspaceRoot}/.env", + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index ad3780d09..c697e013c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "verifyCustomerEmails": "ts-node ./src/commands/verifyCustomerEmails.ts", "repairPermissionData": "ts-node ./src/commands/repairPermissionData.ts", "migrate": "NODE_ENV=command migrate --migrations-dir='./dist/migrations' --store='./db-migrate-store.js' up", + "migrate-win": "set NODE_ENV=command migrate --migrations-dir='./dist/migrations' --store='./db-migrate-store.js' up", "release": "release-it" }, "lint-staged": { diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index c5c575996..3c1795d11 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -172,10 +172,6 @@ const handleSendMessage = async (flowAction: IFlowActionDocument, conversation: requestName = 'replyTwitterDm'; } - if (kind === KIND_CHOICES.WHATSAPP) { - requestName = 'replyWhatsapp'; - } - if (kind === KIND_CHOICES.WHATSPRO) { type = 'whatspro'; requestName = 'replyWhatsPro'; From e4df1b95c42ff82762ec21f8423fdde3047f78e5 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 21 Mar 2020 09:34:57 -0300 Subject: [PATCH 04/71] Implemented user assigned isolation --- src/apolloClient.ts | 6 +++- src/data/resolvers/mutations/conversations.ts | 34 ++++++++++++++++++- .../queries/conversationQueryBuilder.ts | 11 ++++++ src/data/resolvers/queries/conversations.ts | 1 + 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/apolloClient.ts b/src/apolloClient.ts index 1789d696d..8e2824139 100644 --- a/src/apolloClient.ts +++ b/src/apolloClient.ts @@ -80,7 +80,11 @@ const apolloServer = new ApolloServer({ scopeBrandIds = brandIds; } - if (!user.isOwner) { + if (!user.isOwner && scopeBrandIds.length) { + // Select non-existent or empty arrays too + scopeBrandIds.push(null); + scopeBrandIds.push([]); + brandIdSelector = { _id: { $in: scopeBrandIds } }; commonQuerySelector = { scopeBrandIds: { $in: scopeBrandIds } }; commonQuerySelectorElk = { terms: { scopeBrandIds } }; diff --git a/src/data/resolvers/mutations/conversations.ts b/src/data/resolvers/mutations/conversations.ts index 2efafc0d5..c43442872 100644 --- a/src/data/resolvers/mutations/conversations.ts +++ b/src/data/resolvers/mutations/conversations.ts @@ -1,6 +1,6 @@ import * as strip from 'strip'; import * as _ from 'underscore'; -import { ConversationMessages, Conversations, Customers, Integrations } from '../../../db/models'; +import { ConversationMessages, Conversations, Customers, Integrations, Users } from '../../../db/models'; import Messages from '../../../db/models/ConversationMessages'; import { CONVERSATION_STATUSES, @@ -320,6 +320,21 @@ const conversationMutations = { await sendNotifications({ user, conversations, type: NOTIFICATION_TYPES.CONVERSATION_ASSIGNEE_CHANGE }); + // Add bot message and update conversation + let assignedUser = await Users.getUser(assignedUserId); + + for (const conversation of conversations) { + let message = await ConversationMessages.addMessage({ + conversationId: conversation._id, + content: `${assignedUser.details?.shortName || assignedUser.email} has been assigned to this conversation`, + fromBot: true, + }); + + graphqlPubsub.publish('conversationClientMessageInserted', { + conversationClientMessageInserted: message, + }); + } + return conversations; }, @@ -339,6 +354,23 @@ const conversationMutations = { // notify graphl subscription publishConversationsChanged(_ids, 'assigneeChanged'); + // Add bot message and update conversation + for (const conversation of oldConversations) { + if (!conversation.assignedUserId) continue; + + let unAssignedUser = await Users.getUser(conversation.assignedUserId); + let message = await ConversationMessages.addMessage({ + conversationId: conversation._id, + content: `${unAssignedUser.details?.shortName || + unAssignedUser.email} has been deassigned from this conversation`, + fromBot: true, + }); + + graphqlPubsub.publish('conversationClientMessageInserted', { + conversationClientMessageInserted: message, + }); + } + return updatedConversations; }, diff --git a/src/data/resolvers/queries/conversationQueryBuilder.ts b/src/data/resolvers/queries/conversationQueryBuilder.ts index 0275673f4..b1872e44b 100644 --- a/src/data/resolvers/queries/conversationQueryBuilder.ts +++ b/src/data/resolvers/queries/conversationQueryBuilder.ts @@ -3,6 +3,8 @@ import { Channels, Integrations } from '../../../db/models'; import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; import { fixDate } from '../../utils'; +const { USE_CHAT_RESTRICTIONS } = process.env; + interface IIn { $in: string[]; } @@ -29,6 +31,7 @@ export interface IListArgs { interface IUserArgs { _id: string; + isOwner?: boolean; starredConversationIds?: string[]; } @@ -69,6 +72,12 @@ export default class Builder { statusFilter = this.statusFilter([CONVERSATION_STATUSES.CLOSED]); } + let assignedUserQuery: any = {}; + + if (USE_CHAT_RESTRICTIONS === 'true' && !this.user.isOwner) { + assignedUserQuery.assignedUserId = { $in: [this.user._id, null] }; + } + return { ...statusFilter, // exclude engage messages if customer did not reply @@ -76,9 +85,11 @@ export default class Builder { { userId: { $exists: true }, messageCount: { $gt: 1 }, + ...assignedUserQuery, }, { userId: { $exists: false }, + ...assignedUserQuery, }, ], }; diff --git a/src/data/resolvers/queries/conversations.ts b/src/data/resolvers/queries/conversations.ts index 691c5d2af..8e28ff21a 100644 --- a/src/data/resolvers/queries/conversations.ts +++ b/src/data/resolvers/queries/conversations.ts @@ -93,6 +93,7 @@ const conversationQueries = { // initiate query builder const qb = new QueryBuilder(params, { _id: user._id, + isOwner: user.isOwner, starredConversationIds: user.starredConversationIds, }); From 97e00017f72f3a42d880641e66ba639b0408565f Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 26 Mar 2020 08:23:42 -0300 Subject: [PATCH 05/71] Chatbot --- src/__tests__/knowledgeBaseDb.test.ts | 8 +- src/__tests__/pipelineTemplateDb.test.ts | 5 +- .../pipelineTemplateMutations.test.ts | 5 +- src/__tests__/setup.ts | 5 +- src/commands/aftertest.ts | 16 +- src/commands/initProject.ts | 204 ++++++++++++------ src/data/modules/flow/index.ts | 159 ++++++++++++-- .../modules/integrations/receiveMessage.ts | 18 +- src/data/resolvers/queries/channels.ts | 5 +- .../queries/conversationQueryBuilder.ts | 12 +- src/data/resolvers/queries/conversations.ts | 15 +- src/db/models/definitions/conversations.ts | 3 + src/db/models/definitions/flowActions.ts | 2 +- src/workers/utils.ts | 6 +- 14 files changed, 354 insertions(+), 109 deletions(-) diff --git a/src/__tests__/knowledgeBaseDb.test.ts b/src/__tests__/knowledgeBaseDb.test.ts index 405a8c811..95fecb771 100644 --- a/src/__tests__/knowledgeBaseDb.test.ts +++ b/src/__tests__/knowledgeBaseDb.test.ts @@ -209,7 +209,7 @@ describe('test knowledge base models', () => { _id: topicB._id.toString(), }); - if (!topicAObj || !topicAObj.categoryIds || (!topicBObj || !topicBObj.categoryIds)) { + if (!topicAObj || !topicAObj.categoryIds || !topicBObj || !topicBObj.categoryIds) { throw new Error('Topic not found'); } @@ -277,7 +277,7 @@ describe('test knowledge base models', () => { _id: topicB._id.toString(), }); - if (!topicAObj || !topicAObj.categoryIds || (!topicBObj || !topicBObj.categoryIds)) { + if (!topicAObj || !topicAObj.categoryIds || !topicBObj || !topicBObj.categoryIds) { throw new Error('Topic not found'); } @@ -395,7 +395,7 @@ describe('test knowledge base models', () => { _id: categoryB._id.toString(), }); - if (!categoryAObj || !categoryAObj.articleIds || (!categoryBObj || !categoryBObj.articleIds)) { + if (!categoryAObj || !categoryAObj.articleIds || !categoryBObj || !categoryBObj.articleIds) { throw new Error('Topic not found'); } @@ -449,7 +449,7 @@ describe('test knowledge base models', () => { _id: categoryB._id.toString(), }); - if (!categoryAObj || !categoryAObj.articleIds || (!categoryBObj || !categoryBObj.articleIds)) { + if (!categoryAObj || !categoryAObj.articleIds || !categoryBObj || !categoryBObj.articleIds) { throw new Error('Topic not found'); } diff --git a/src/__tests__/pipelineTemplateDb.test.ts b/src/__tests__/pipelineTemplateDb.test.ts index d5896f16e..6ba0444a4 100644 --- a/src/__tests__/pipelineTemplateDb.test.ts +++ b/src/__tests__/pipelineTemplateDb.test.ts @@ -88,7 +88,10 @@ describe('Test pipeline template model', () => { // Creating test data const template = await pipelineTemplateFactory({ - stages: [{ name: 'stage 1', formId: form1._id }, { name: 'stage 2', formId: form2._id }], + stages: [ + { name: 'stage 1', formId: form1._id }, + { name: 'stage 2', formId: form2._id }, + ], }); const duplicated = await PipelineTemplates.duplicatePipelineTemplate(template._id); diff --git a/src/__tests__/pipelineTemplateMutations.test.ts b/src/__tests__/pipelineTemplateMutations.test.ts index 7081c7890..fc98b4ded 100644 --- a/src/__tests__/pipelineTemplateMutations.test.ts +++ b/src/__tests__/pipelineTemplateMutations.test.ts @@ -133,7 +133,10 @@ describe('PipelineTemplates mutations', () => { // Creating test data const template = await pipelineTemplateFactory({ - stages: [{ name: 'stage 1', formId: form1._id }, { name: 'stage 2', formId: form2._id }], + stages: [ + { name: 'stage 1', formId: form1._id }, + { name: 'stage 2', formId: form2._id }, + ], }); const duplicated = await graphqlRequest(mutation, 'pipelineTemplatesDuplicate', { _id: template._id }); diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts index 6afb49233..6a13b4b81 100644 --- a/src/__tests__/setup.ts +++ b/src/__tests__/setup.ts @@ -12,10 +12,7 @@ beforeAll(async () => { const mongoUri = await mongoServer.getConnectionString(); - await mongoose.connect( - mongoUri, - { ...connectionOptions, useUnifiedTopology: true }, - ); + await mongoose.connect(mongoUri, { ...connectionOptions, useUnifiedTopology: true }); }); afterAll(async () => { diff --git a/src/commands/aftertest.ts b/src/commands/aftertest.ts index d36ea49ec..56e8d2b3a 100644 --- a/src/commands/aftertest.ts +++ b/src/commands/aftertest.ts @@ -13,10 +13,10 @@ const TEST_MONGO_URL = process.env.TEST_MONGO_URL || 'mongodb://localhost/test'; mongoose.set('useFindAndModify', false); const removeDbs = async () => { - await mongoose.connect( - TEST_MONGO_URL.replace('test', `erxes-test-${Math.random()}`).replace(/\./g, ''), - { useNewUrlParser: true, useCreateIndex: true }, - ); + await mongoose.connect(TEST_MONGO_URL.replace('test', `erxes-test-${Math.random()}`).replace(/\./g, ''), { + useNewUrlParser: true, + useCreateIndex: true, + }); const result = await mongoose.connection.db.admin().command({ listDatabases: 1, @@ -27,10 +27,10 @@ const removeDbs = async () => { const promises: any[] = []; for (const { name } of result.databases) { - const db = await mongoose.connect( - TEST_MONGO_URL.replace('test', name), - { useNewUrlParser: true, useCreateIndex: true }, - ); + const db = await mongoose.connect(TEST_MONGO_URL.replace('test', name), { + useNewUrlParser: true, + useCreateIndex: true, + }); promises.push(db.connection.dropDatabase()); } diff --git a/src/commands/initProject.ts b/src/commands/initProject.ts index 4a76cf484..a00aaef1c 100644 --- a/src/commands/initProject.ts +++ b/src/commands/initProject.ts @@ -40,6 +40,7 @@ connect() if (!flowActionTypes || !flowActionTypes.length) { flowActionTypes = [ + 'erxes.action.root', 'erxes.action.define.department', 'erxes.action.define.tabulation', 'erxes.action.transfer.to.agent', @@ -74,79 +75,162 @@ connect() return FlowActionTypes.insertMany(flowActionTypes); } - - return; }) .then(async () => { - let flow = await Flows.createFlow({ - assignedUserId: 'wJjxQipSXXHbNyqFZ', - name: 'Teste', - description: 'Flow de teste', - }); + let rootFlow = await Flows.findOne({ name: 'Root' }); + + if (!rootFlow) { + rootFlow = await Flows.createFlow({ + assignedUserId: 'wJjxQipSXXHbNyqFZ', + name: 'Root', + description: '', + }); + } + + let secondFlow = await Flows.findOne({ name: 'Second' }); + + if (!secondFlow) { + secondFlow = await Flows.createFlow({ + assignedUserId: 'wJjxQipSXXHbNyqFZ', + name: 'Second', + description: '', + }); + } - const sendMessageType = await FlowActionTypes.findOne({ - type: 'erxes.action.send.message', - }); const askType = await FlowActionTypes.findOne({ type: 'erxes.action.to.ask', }); - await FlowActions.createFlowAction({ - order: 0, - type: sendMessageType?.type, - flowId: flow.id, - actionId: sendMessageType?.id, - value: JSON.stringify({ - content: [ - 'Bem vindo, este é um teste de automação do WhatsBot, converse com o bot respondendo as perguntas que receber', - 'Olá, converse com o bot respondendo as perguntas que receber', - ], - }), + const sendMessageType = await FlowActionTypes.findOne({ + type: 'erxes.action.send.message', }); - await FlowActions.createFlowAction({ - order: 1, - type: askType?.type, - flowId: flow.id, - actionId: askType?.id, - value: JSON.stringify({ - content: [ - 'Com qual setor você gostaria de conversar? 1 - Administrativo 2 - Financeiro - 3 - Sobre', - 'Escolha abaixo o setor que melhor lhe atente? 1 - Administrativo 2 - Financeiro - 3 - Sobre', - ], - conditions: [ - { - operator: '=', - values: ['1', 'administrativo', 'adm'], - action: 'erxes.action.choose.department', - value: 'Administrativo', - }, - { - operator: '=', - values: ['2', 'financeiro'], - action: 'erxes.action.choose.department', - value: 'Financeiro', - }, - { - operator: '=', - values: ['3', 'sobre'], - action: 'erxes.action.execute.action', - value: '2', - }, - ], - }), + const defineDepartmenType = await FlowActionTypes.findOne({ + type: 'erxes.action.define.department', }); - await FlowActions.createFlowAction({ - order: 2, - type: sendMessageType?.type, - flowId: flow.id, - actionId: sendMessageType?.id, - value: JSON.stringify({ - content: ['Este é um teste de automação do WhatsBot'], - }), + const executeFlowType = await FlowActionTypes.findOne({ + type: 'erxes.action.execute.autmations.flow', }); + + if (!(await FlowActions.count({ flowId: rootFlow.id }))) { + await FlowActions.createFlowAction({ + order: 0, + type: askType?.type, + flowId: rootFlow.id, + actionId: askType?.id, + value: JSON.stringify({ + content: [ + 'Olá, tudo bem?' + + '
Muito bom vê-lo aqui! 😊
' + + '
Bom, para agilizar seu atendimento, por favor digite a opção desejada:' + + '
1 - Ainda não sou cliente, quero falar com o Comercial.' + + '
2 - Já sou cliente, quero falar com o Suporte pois tenho dúvidas ou preciso de algo', + ], + conditions: [ + { + operator: '=', + values: ['1', 'comercial', 'vendas', 'venda'], + action: 'erxes.action.execute.action', + value: '1', + }, + { + operator: '=', + values: ['2', 'suporte'], + action: 'erxes.action.execute.action', + value: '3', + }, + ], + }), + }); + + await FlowActions.createFlowAction({ + order: 1, + flowId: rootFlow.id, + type: defineDepartmenType?.type, + actionId: defineDepartmenType?.id, + value: 'Rs8GERDMd4PK5xnKv', + }); + + await FlowActions.createFlowAction({ + order: 2, + flowId: rootFlow.id, + type: executeFlowType?.type, + actionId: executeFlowType?.id, + value: secondFlow.id, + }); + + await FlowActions.createFlowAction({ + order: 3, + flowId: rootFlow.id, + type: defineDepartmenType?.type, + actionId: defineDepartmenType?.id, + value: 'RybHspzXFcc2GPtG4', + }); + + await FlowActions.createFlowAction({ + order: 4, + flowId: rootFlow.id, + type: executeFlowType?.type, + actionId: executeFlowType?.id, + value: secondFlow.id, + }); + } + + if (!(await FlowActions.count({ flowId: secondFlow.id }))) { + await FlowActions.createFlowAction({ + order: 0, + flowId: secondFlow.id, + type: askType?.type, + actionId: askType?.id, + value: JSON.stringify({ + content: [ + '
Legal, por favor digite a opção desejada:' + + '
1 - Informações sobre MEI e nossos serviços' + + '
2 - Falar com um atendente', + ], + conditions: [ + { + operator: '=', + values: [ + '1', + 'mei', + 'servicos', + 'serviços', + 'servico', + 'serviço', + 'info', + 'informações', + 'informacoes', + 'infos', + 'sobre', + ], + action: 'erxes.action.execute.action', + value: '1', + }, + { + operator: '=', + values: ['2', 'falar', 'atendente', 'atendimento'], + action: 'erxes.action.transfer.to.agent', + value: '', + }, + ], + }), + }); + + await FlowActions.createFlowAction({ + order: 1, + flowId: secondFlow.id, + type: sendMessageType?.type, + actionId: sendMessageType?.id, + value: JSON.stringify({ + content: [ + 'Para saber mais sobre MEI e nossos serviços acesse:
' + 'https://dues.gpages.com.br/pagina-captura', + ], + }), + }); + } }) .then(() => { diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 3c1795d11..d6454b6bb 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -7,6 +7,7 @@ import { Users, FlowActions, Customers, + Channels, } from '../../../db/models'; import { IntegrationsAPI } from '../../dataSources'; @@ -20,10 +21,12 @@ import { import { IConversationDocument } from '../../../db/models/definitions/conversations'; import { sendMessage } from '../../../messageBroker'; import { KIND_CHOICES } from '../../../db/models/definitions/constants'; -import { IConversationMessageAdd } from '../../resolvers/mutations/conversations'; +import { IConversationMessageAdd, publishConversationsChanged } from '../../resolvers/mutations/conversations'; import { graphqlPubsub } from '../../../pubsub'; import Messages from '../../../db/models/ConversationMessages'; +const actionWithSendNext = ['erxes.action.send.message', 'erxes.action.define.department']; + const handleCondition = (condition: IFlowActionValueCondition, content: string = '') => { switch (condition.operator) { case '=': @@ -34,30 +37,75 @@ const handleCondition = (condition: IFlowActionValueCondition, content: string = }; const handleMessage = async (msg: IMessageDocument) => { - const conversation = await Conversations.getConversation(msg.conversationId); + let conversation = await Conversations.getConversation(msg.conversationId); if (!conversation) return; const integration = await Integrations.getIntegration(conversation?.integrationId); - if (!integration) return; + if (!integration || !integration.flowId) return; + + const flow = await Flows.getFlow(integration.flowId); + + if (!flow || (conversation.assignedUserId && conversation.assignedUserId !== flow.assignedUserId)) return; + + const user = await Users.findById(flow.assignedUserId); + + if (!user) return; + + if (!conversation.assignedUserId) { + const conversations: IConversationDocument[] = await Conversations.assignUserConversation( + [conversation.id], + user.id, + ); + + conversation = conversations[0]; + + // notify graphl subscription + publishConversationsChanged([conversation.id], 'assigneeChanged'); + + for (const conversation of conversations) { + let message = await ConversationMessages.addMessage({ + conversationId: conversation._id, + content: `${user.details?.shortName || user.email} has been assigned to this conversation`, + fromBot: true, + }); + + graphqlPubsub.publish('conversationClientMessageInserted', { + conversationClientMessageInserted: message, + }); + } + } let flowAction: IFlowActionDocument | null = null; let sendNextMessage = false; if (conversation?.currentFlowActionId) { - flowAction = await FlowActions.getFlowAction(conversation.currentFlowActionId); + flowAction = await FlowActions.findById(conversation.currentFlowActionId); if (flowAction) { switch (flowAction.type) { + case 'erxes.action.root': + case 'erxes.action.define.department': case 'erxes.action.send.message': flowAction = await FlowActions.findOne({ - flowId: integration.flowId, + flowId: flowAction.flowId, order: flowAction.order + 1, }); - if (flowAction?.type === 'erxes.action.send.message') sendNextMessage = true; + // if (actionWithSendNext.includes(flowAction?.type || "")) + // sendNextMessage = true; + break; + case 'erxes.action.execute.autmations.flow': + flowAction = await FlowActions.findOne({ + flowId: flowAction?.value, + order: 0, + }); + + await Conversations.updateConversation(conversation.id, { + currentFlowActionId: flowAction?.id, + }); break; case 'erxes.action.to.ask': const { conditions }: IFlowActionValue = JSON.parse(flowAction.value || '{}'); @@ -68,10 +116,65 @@ const handleMessage = async (msg: IMessageDocument) => { switch (condition.action) { case 'erxes.action.execute.action': flowAction = await FlowActions.findOne({ - flowId: integration.flowId, + flowId: flowAction.flowId, order: condition.value, }); + break; + case 'erxes.action.transfer.to.agent': + let assignedUserId: string = ''; + + let channel = await Channels.findById(conversation.channelId); + + if (channel && channel.memberIds?.length) { + let position = channel.memberIds.length - 1; + + position = Math.round(position * Math.random()); + + assignedUserId = channel.memberIds[position]; + } + + if (!assignedUserId) { + let users = await Users.aggregate([ + { + $match: { + brandIds: { $in: [integration.brandId] }, + }, + }, + { $sample: { size: 1 } }, + ]); + + if (users?.length) { + assignedUserId = users[0].id; + } + } + + if (assignedUserId) { + const conversations: IConversationDocument[] = await Conversations.assignUserConversation( + [conversation.id], + assignedUserId, + ); + + // notify graphl subscription + publishConversationsChanged([conversation.id], 'assigneeChanged'); + + let assignedUser = await Users.getUser(assignedUserId); + + for (const conversation of conversations) { + let message = await ConversationMessages.addMessage({ + conversationId: conversation._id, + content: `${assignedUser.details?.shortName || + assignedUser.email} has been assigned to this conversation`, + fromBot: true, + }); + + graphqlPubsub.publish('conversationClientMessageInserted', { + conversationClientMessageInserted: message, + }); + } + } + + return; default: break; } @@ -86,11 +189,11 @@ const handleMessage = async (msg: IMessageDocument) => { if (!flowAction) { if (integration?.flowId) { flowAction = await FlowActions.findOne({ - flowId: integration.flowId, + flowId: flow.id, order: 0, }); - sendNextMessage = true; + if (actionWithSendNext.includes(flowAction?.type || '')) sendNextMessage = true; } } @@ -103,11 +206,39 @@ const handleMessage = async (msg: IMessageDocument) => { switch (flowAction.type) { case 'erxes.action.send.message': await handleSendMessage(flowAction, conversation); - break; + if ( + await FlowActions.findOne({ + flowId: flowAction.flowId, + order: flowAction.order + 1, + }) + ) + sendNextMessage = true; + + break; case 'erxes.action.to.ask': await handleSendMessage(flowAction, conversation); break; + + case 'erxes.action.define.department': + Conversations.updateConversation(conversation.id, { + channelId: flowAction.value, + }); + + if ( + await FlowActions.findOne({ + flowId: flowAction.flowId, + order: flowAction.order + 1, + }) + ) + sendNextMessage = true; + + break; + case 'erxes.action.execute.autmations.flow': + sendNextMessage = true; + + break; + default: break; } @@ -140,9 +271,9 @@ const handleSendMessage = async (flowAction: IFlowActionDocument, conversation: const { content }: IFlowActionValue = JSON.parse(flowAction.value || '[]'); - const length = content.length - 1; - const random = Math.random(); - const position = Math.round(length * random); + let position = content.length - 1; + + position = Math.round(position * Math.random()); const doc: IConversationMessageAdd = { conversationId, @@ -224,8 +355,6 @@ const sendConversationToIntegrations = ( doc.content = doc.content.replace(/
/g, '\n'); doc.content = doc.content.replace(/<\/?i>/g, '_'); doc.content = doc.content.replace(/<\/?s>/g, '~'); - - doc.content += '\n\n```WhatsPro Bot```'; } if (dataSources && dataSources.IntegrationsAPI && requestName) { diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index 8fca7709b..f2c9f26dc 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -70,15 +70,20 @@ export const receiveRpcMessage = async msg => { const assignedUserId = user ? user._id : null; - if (conversationId) { - await Conversations.updateConversation(conversationId, { content, assignedUserId }); + let conversation = await Conversations.findById(conversationId); + + if (conversation && conversation.status !== 'closed') { + await Conversations.updateConversation(conversationId, { + content, + assignedUserId: conversation.assignedUserId || assignedUserId, + }); return sendSuccess({ _id: conversationId }); } doc.assignedUserId = assignedUserId; - const conversation = await Conversations.createConversation(doc); + conversation = await Conversations.createConversation(doc); return sendSuccess({ _id: conversation._id }); } @@ -86,7 +91,12 @@ export const receiveRpcMessage = async msg => { if (action === 'create-conversation-message') { const message = await ConversationMessages.createMessage(doc); - const conversationDoc: { status: string; readUserIds: string[]; content?: string; updatedAt?: Date } = { + const conversationDoc: { + status: string; + readUserIds: string[]; + content?: string; + updatedAt?: Date; + } = { // Reopen its conversation if it's closed status: doc.unread || doc.unread === undefined ? CONVERSATION_STATUSES.OPEN : CONVERSATION_STATUSES.CLOSED, diff --git a/src/data/resolvers/queries/channels.ts b/src/data/resolvers/queries/channels.ts index 9a14f03ca..6bf38e344 100644 --- a/src/data/resolvers/queries/channels.ts +++ b/src/data/resolvers/queries/channels.ts @@ -1,5 +1,6 @@ import { Channels } from '../../../db/models'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; interface IIn { $in: string[]; @@ -13,11 +14,11 @@ const channelQueries = { /** * Channels list */ - channels(_root, { memberIds }: { memberIds: string[] }) { + channels(_root, { memberIds }: { memberIds: string[] }, { user }: IContext) { const query: IChannelQuery = {}; const sort = { createdAt: -1 }; - if (memberIds) { + if (!user.isOwner && memberIds?.length) { query.memberIds = { $in: memberIds }; } diff --git a/src/data/resolvers/queries/conversationQueryBuilder.ts b/src/data/resolvers/queries/conversationQueryBuilder.ts index b1872e44b..a21f1946e 100644 --- a/src/data/resolvers/queries/conversationQueryBuilder.ts +++ b/src/data/resolvers/queries/conversationQueryBuilder.ts @@ -153,7 +153,9 @@ export default class Builder { const channel = await Channels.getChannel(channelId); return { - integrationId: { $in: (channel.integrationIds || []).filter(id => this.activeIntegrationIds.includes(id)) }, + integrationId: { + $in: (channel.integrationIds || []).filter(id => this.activeIntegrationIds.includes(id)), + }, }; } @@ -200,7 +202,9 @@ export default class Builder { // filter by integration type public async integrationTypeFilter(integrationType: string): Promise<{ $and: IIntersectIntegrationIds[] }> { - const integrations = await Integrations.findIntegrations({ kind: integrationType }); + const integrations = await Integrations.findIntegrations({ + kind: integrationType, + }); return { $and: [ @@ -257,6 +261,10 @@ export default class Builder { // filter by channelId & brandId this.queries.integrations = await this.integrationsFilter(); + if (!this.queries.integrations.integrationId.$in.length) { + this.queries.integrations = {}; + } + // unassigned if (this.params.unassigned) { this.queries.unassigned = this.unassignedFilter(); diff --git a/src/data/resolvers/queries/conversations.ts b/src/data/resolvers/queries/conversations.ts index 8e28ff21a..5c35234d4 100644 --- a/src/data/resolvers/queries/conversations.ts +++ b/src/data/resolvers/queries/conversations.ts @@ -99,7 +99,8 @@ const conversationQueries = { await qb.buildAllQueries(); - return Conversations.find(qb.mainQuery()) + const query = qb.mainQuery(); + return Conversations.find(query) .sort({ updatedAt: -1 }) .limit(params.limit || 0); }, @@ -172,6 +173,7 @@ const conversationQueries = { const qb = new QueryBuilder(params, { _id: user._id, + isOwner: user.isOwner, starredConversationIds: user.starredConversationIds, }); @@ -246,6 +248,7 @@ const conversationQueries = { // initiate query builder const qb = new QueryBuilder(params, { _id: user._id, + isOwner: user.isOwner, starredConversationIds: user.starredConversationIds, }); @@ -261,6 +264,7 @@ const conversationQueries = { // initiate query builder const qb = new QueryBuilder(params, { _id: user._id, + isOwner: user.isOwner, starredConversationIds: user.starredConversationIds, }); @@ -274,7 +278,14 @@ const conversationQueries = { */ async conversationsTotalUnreadCount(_root, _args, { user }: IContext) { // initiate query builder - const qb = new QueryBuilder({}, { _id: user._id }); + const qb = new QueryBuilder( + {}, + { + _id: user._id, + isOwner: user.isOwner, + }, + ); + await qb.buildAllQueries(); // get all possible integration ids diff --git a/src/db/models/definitions/conversations.ts b/src/db/models/definitions/conversations.ts index 59d57eb36..440e0b17e 100644 --- a/src/db/models/definitions/conversations.ts +++ b/src/db/models/definitions/conversations.ts @@ -27,6 +27,7 @@ export interface IConversation { firstRespondedDate?: Date; currentFlowActionId?: string; + channelId?: string; } // Conversation schema @@ -74,4 +75,6 @@ export const conversationSchema = new Schema({ firstRespondedDate: field({ type: Date }), currentFlowActionId: field({ type: String }), + + channelId: field({ type: String }), }); diff --git a/src/db/models/definitions/flowActions.ts b/src/db/models/definitions/flowActions.ts index 894a8d896..25c2ad280 100644 --- a/src/db/models/definitions/flowActions.ts +++ b/src/db/models/definitions/flowActions.ts @@ -38,7 +38,7 @@ export const flowActionSchema = new Schema({ flowId: field({ type: String, label: 'Flow' }), actionId: field({ type: String, label: 'Flow Action Type' }), userId: field({ type: String, label: 'Created by' }), - value: field({ type: String, label: 'Value' }), + value: field({ type: String, optional: true, label: 'Value' }), order: field({ type: Number, label: 'Order' }), createdAt: field({ type: Date, label: 'Created at' }), }); diff --git a/src/workers/utils.ts b/src/workers/utils.ts index 4230ea28c..114f8230c 100644 --- a/src/workers/utils.ts +++ b/src/workers/utils.ts @@ -97,8 +97,4 @@ export const clearIntervals = () => { const { MONGO_URL = '' } = process.env; -export const connect = () => - mongoose.connect( - MONGO_URL, - { useNewUrlParser: true, useCreateIndex: true }, - ); +export const connect = () => mongoose.connect(MONGO_URL, { useNewUrlParser: true, useCreateIndex: true }); From e408dcec12053231d4fe64d29cda5c4c6186ddfd Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 4 Apr 2020 17:01:53 -0300 Subject: [PATCH 06/71] fix issue when no file type is specified --- src/data/utils.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/data/utils.ts b/src/data/utils.ts index 86e344aa2..e659e1417 100644 --- a/src/data/utils.ts +++ b/src/data/utils.ts @@ -40,21 +40,11 @@ export const checkFile = async (file, source?: string) => { return 'Invalid file type'; } - const defaultMimeTypes = [ - 'image/png', - 'image/jpeg', - 'image/jpg', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/pdf', - 'image/gif', - ]; - const UPLOAD_FILE_TYPES = await getConfig(source === 'widgets' ? 'WIDGETS_UPLOAD_FILE_TYPES' : 'UPLOAD_FILE_TYPES'); const { mime } = ft; - if (!((UPLOAD_FILE_TYPES && UPLOAD_FILE_TYPES.split(',')) || defaultMimeTypes).includes(mime)) { + if (UPLOAD_FILE_TYPES && !UPLOAD_FILE_TYPES.split(',').includes(mime)) { return 'Invalid configured file type'; } From 80300cafe119d53bba79e386a9fbd497e6e3e0b9 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sun, 5 Apr 2020 20:47:17 -0300 Subject: [PATCH 07/71] add vcard message type --- src/db/models/definitions/constants.ts | 30 +++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/db/models/definitions/constants.ts b/src/db/models/definitions/constants.ts index 427fa0d9d..05448a514 100644 --- a/src/db/models/definitions/constants.ts +++ b/src/db/models/definitions/constants.ts @@ -66,6 +66,11 @@ export const KIND_CHOICES = { CALLPRO: 'callpro', TWITTER_DM: 'twitter-dm', CHATFUEL: 'chatfuel', + SMOOCH_VIBER: 'smooch-viber', + SMOOCH_LINE: 'smooch-line', + SMOOCH_TELEGRAM: 'smooch-telegram', + SMOOCH_TWILIO: 'smooch-twilio', + WHATSAPP: 'whatsapp', WHATSPRO: 'whatspro', ALL: [ 'messenger', @@ -81,6 +86,11 @@ export const KIND_CHOICES = { 'nylas-outlook', 'nylas-yahoo', 'twitter-dm', + 'smooch-viber', + 'smooch-line', + 'smooch-telegram', + 'smooch-twilio', + 'whatsapp', 'whatspro', ], }; @@ -99,6 +109,11 @@ export const INTEGRATION_NAMES_MAP = { 'nylas-outlook': 'Outook', 'nylas-yahoo': 'Yahoo', 'twitter-dm': 'Twitter dm', + 'smooch-viber': 'Viber', + 'smooch-line': 'Line', + 'smooch-telegram': 'Telegram', + 'smooch-twilio': 'Twilio SMS', + whatsapp: 'WhatsApp', }; // messenger data availability constants @@ -205,17 +220,7 @@ export const FIELDS_GROUPS_CONTENT_TYPES = { ALL: ['customer', 'company', 'product'], }; -export const CUSTOMER_LEAD_STATUS_TYPES = [ - '', - 'new', - 'open', - 'inProgress', - 'openDeal', - 'unqualified', - 'attemptedToContact', - 'connected', - 'badTiming', -]; +export const CUSTOMER_LEAD_STATUS_TYPES = ['', 'new', 'contacte', 'working', 'openDeal', 'unqualified']; export const CUSTOMER_LIFECYCLE_STATE_TYPES = [ '', @@ -366,7 +371,8 @@ export const MESSAGE_TYPES = { VIDEO_CALL: 'videoCall', VIDEO_CALL_REQUEST: 'videoCallRequest', TEXT: 'text', - ALL: ['videoCall', 'videoCallRequest', 'text'], + VCARD: 'vcard', + ALL: ['videoCall', 'videoCallRequest', 'text', 'vcard'], }; // module constants From 43b95b7cee5a49460e8ecd79c09e681c7d4ad378 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 10 Apr 2020 07:51:25 -0300 Subject: [PATCH 08/71] ignore old msgs and group message on flow --- src/data/modules/flow/index.ts | 2 ++ src/data/schema/conversation.ts | 1 + src/db/models/definitions/conversationMessages.ts | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index d6454b6bb..d2adc8568 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -37,6 +37,8 @@ const handleCondition = (condition: IFlowActionValueCondition, content: string = }; const handleMessage = async (msg: IMessageDocument) => { + if (msg.isGroupMsg || !msg.content) return; + let conversation = await Conversations.getConversation(msg.conversationId); if (!conversation) return; diff --git a/src/data/schema/conversation.ts b/src/data/schema/conversation.ts index c617f470b..0a041d98e 100644 --- a/src/data/schema/conversation.ts +++ b/src/data/schema/conversation.ts @@ -65,6 +65,7 @@ export const types = ` conversationId: String internal: Boolean fromBot: Boolean + isGroupMsg: Boolean customerId: String userId: String createdAt: Date diff --git a/src/db/models/definitions/conversationMessages.ts b/src/db/models/definitions/conversationMessages.ts index 4180edce0..4cb24723f 100644 --- a/src/db/models/definitions/conversationMessages.ts +++ b/src/db/models/definitions/conversationMessages.ts @@ -35,6 +35,7 @@ export interface IMessage { customerId?: string; userId?: string; fromBot?: boolean; + isGroupMsg?: boolean; isCustomerRead?: boolean; formWidgetData?: any; messengerAppData?: any; @@ -91,6 +92,7 @@ export const messageSchema = new Schema({ internal: field({ type: Boolean, index: true }), customerId: field({ type: String, index: true }), fromBot: field({ type: Boolean }), + isGroupMsg: field({ type: Boolean }), userId: field({ type: String, index: true }), createdAt: field({ type: Date, index: true }), isCustomerRead: field({ type: Boolean }), From 2a7ffcaf8781b978bca0f8f12bf05da50b6f5183 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 10 Apr 2020 14:52:47 -0300 Subject: [PATCH 09/71] improved reset flow --- src/data/modules/flow/index.ts | 71 +++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index d2adc8568..5603294d0 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -1,3 +1,4 @@ +import * as moment from 'moment'; import * as strip from 'strip'; import { ConversationMessages, @@ -24,6 +25,8 @@ import { KIND_CHOICES } from '../../../db/models/definitions/constants'; import { IConversationMessageAdd, publishConversationsChanged } from '../../resolvers/mutations/conversations'; import { graphqlPubsub } from '../../../pubsub'; import Messages from '../../../db/models/ConversationMessages'; +import { IIntegrationDocument } from '../../../db/models/definitions/integrations'; +import { IDetail, IUserDocument } from '../../../db/models/definitions/users'; const actionWithSendNext = ['erxes.action.send.message', 'erxes.action.define.department']; @@ -49,7 +52,24 @@ const handleMessage = async (msg: IMessageDocument) => { const flow = await Flows.getFlow(integration.flowId); - if (!flow || (conversation.assignedUserId && conversation.assignedUserId !== flow.assignedUserId)) return; + if (!flow) return; + + if (conversation.assignedUserId) { + if (conversation.assignedUserId !== flow.assignedUserId) { + let lastMessage = await ConversationMessages.findOne({ + userId: conversation.assignedUserId, + conversationId: conversation.id, + }) + .sort({ createdAt: -1 }) + .exec(); + + if (lastMessage && moment(lastMessage.createdAt).isAfter(moment().subtract(30, 'minutes'))) { + return; + } + + conversation.assignedUserId = undefined; + } + } const user = await Users.findById(flow.assignedUserId); @@ -62,6 +82,7 @@ const handleMessage = async (msg: IMessageDocument) => { ); conversation = conversations[0]; + conversation.currentFlowActionId = undefined; // notify graphl subscription publishConversationsChanged([conversation.id], 'assigneeChanged'); @@ -173,6 +194,29 @@ const handleMessage = async (msg: IMessageDocument) => { graphqlPubsub.publish('conversationClientMessageInserted', { conversationClientMessageInserted: message, }); + + if (condition.value) { + let content = condition.value; + let details: IDetail = assignedUser.details?.toObject() || {}; + + const keys = Object.keys(details); + + for (const key of keys) { + content = content.replace(new RegExp(`{{${key}}}`), details[key]); + } + + handleSendMessage( + integration, + conversation, + { + conversationId: conversation.id, + flowActionId: flowAction.id, + internal: false, + content, + }, + assignedUser, + ); + } } } @@ -207,7 +251,7 @@ const handleMessage = async (msg: IMessageDocument) => { switch (flowAction.type) { case 'erxes.action.send.message': - await handleSendMessage(flowAction, conversation); + await proccessSendMessage(flowAction, conversation); if ( await FlowActions.findOne({ @@ -219,7 +263,7 @@ const handleMessage = async (msg: IMessageDocument) => { break; case 'erxes.action.to.ask': - await handleSendMessage(flowAction, conversation); + await proccessSendMessage(flowAction, conversation); break; case 'erxes.action.define.department': @@ -250,15 +294,11 @@ const handleMessage = async (msg: IMessageDocument) => { } }; -const handleSendMessage = async (flowAction: IFlowActionDocument, conversation: IConversationDocument) => { +const proccessSendMessage = async (flowAction: IFlowActionDocument, conversation: IConversationDocument) => { const integration = await Integrations.getIntegration(conversation.integrationId); if (!integration) return; - const kind = integration.kind; - const integrationId = integration.id; - const conversationId = conversation.id; - const customer = await Customers.findById(conversation.customerId); if (!customer) return; @@ -278,14 +318,27 @@ const handleSendMessage = async (flowAction: IFlowActionDocument, conversation: position = Math.round(position * Math.random()); const doc: IConversationMessageAdd = { - conversationId, + conversationId: conversation.id, flowActionId: flowAction.id, internal: false, content: content[position], }; + handleSendMessage(integration, conversation, doc, user); +}; + +const handleSendMessage = async ( + integration: IIntegrationDocument, + conversation: IConversationDocument, + doc: any, + user: IUserDocument, +) => { const message = await ConversationMessages.addMessage(doc, user._id); + const kind = integration.kind; + const integrationId = integration.id; + const conversationId = conversation.id; + let requestName; let type; let action; From f2c5a1b8ed14ea9375121875a8dcdeeb05cad1e5 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Wed, 15 Apr 2020 16:47:30 -0300 Subject: [PATCH 10/71] Fix send multiple messages issue --- src/data/modules/flow/index.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 5603294d0..d6aafaa7b 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -63,7 +63,7 @@ const handleMessage = async (msg: IMessageDocument) => { .sort({ createdAt: -1 }) .exec(); - if (lastMessage && moment(lastMessage.createdAt).isAfter(moment().subtract(30, 'minutes'))) { + if (lastMessage && moment(lastMessage.createdAt).isAfter(moment().subtract(1, 'day'))) { return; } @@ -317,6 +317,20 @@ const proccessSendMessage = async (flowAction: IFlowActionDocument, conversation position = Math.round(position * Math.random()); + let lastMessage = await ConversationMessages.findOne({ + userId: conversation.assignedUserId, + conversationId: conversation.id, + }) + .sort({ createdAt: -1 }) + .exec(); + + if ( + lastMessage && + content.includes(lastMessage.content || '') && + moment(lastMessage.createdAt).isAfter(moment().subtract(30, 'minutes')) + ) + return; + const doc: IConversationMessageAdd = { conversationId: conversation.id, flowActionId: flowAction.id, From d2c9f8eebc7495b6cdab915e3d62759c5312baea Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 17 Apr 2020 07:05:34 -0300 Subject: [PATCH 11/71] Last seen --- src/apolloClient.ts | 11 +++++- src/data/modules/flow/index.ts | 43 +++++++++++++++++++++--- src/data/schema/user.ts | 1 + src/db/models/definitions/flowActions.ts | 1 + src/db/models/definitions/users.ts | 30 ++++++++++++++--- 5 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/apolloClient.ts b/src/apolloClient.ts index 2adc02fd6..8ce264876 100644 --- a/src/apolloClient.ts +++ b/src/apolloClient.ts @@ -3,7 +3,7 @@ import * as dotenv from 'dotenv'; import { EngagesAPI, IntegrationsAPI } from './data/dataSources'; import resolvers from './data/resolvers'; import typeDefs from './data/schema'; -import { Conversations, Customers } from './db/models'; +import { Conversations, Customers, Users } from './db/models'; import { graphqlPubsub } from './pubsub'; import { addToArray, get, inArray, removeFromArray, set } from './redisClient'; @@ -55,6 +55,15 @@ const apolloServer = new ApolloServer({ const user = req.user; + if (user) { + Users.update( + { _id: user._id }, + { + lastSeenAt: new Date(), + }, + ).exec(); + } + if (USE_BRAND_RESTRICTIONS !== 'true') { return { brandIdSelector: {}, diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index d6aafaa7b..8485061e0 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -150,11 +150,23 @@ const handleMessage = async (msg: IMessageDocument) => { let channel = await Channels.findById(conversation.channelId); if (channel && channel.memberIds?.length) { - let position = channel.memberIds.length - 1; - - position = Math.round(position * Math.random()); + let users = await Users.aggregate([ + { + $match: { + _id: { $in: channel.memberIds }, + lastSeenAt: { + $gte: moment() + .subtract(61, 'seconds') + .toDate(), + }, + }, + }, + { $sample: { size: 1 } }, + ]); - assignedUserId = channel.memberIds[position]; + if (users?.length) { + assignedUserId = users[0]._id; + } } if (!assignedUserId) { @@ -162,13 +174,18 @@ const handleMessage = async (msg: IMessageDocument) => { { $match: { brandIds: { $in: [integration.brandId] }, + lastSeenAt: { + $gte: moment() + .subtract(61, 'seconds') + .toDate(), + }, }, }, { $sample: { size: 1 } }, ]); if (users?.length) { - assignedUserId = users[0].id; + assignedUserId = users[0]._id; } } @@ -218,6 +235,22 @@ const handleMessage = async (msg: IMessageDocument) => { ); } } + } else if (condition.error) { + const user = await Users.findById(flow?.assignedUserId); + + if (user) { + handleSendMessage( + integration, + conversation, + { + conversationId: conversation.id, + flowActionId: flowAction.id, + internal: false, + content: condition.error, + }, + user, + ); + } } return; diff --git a/src/data/schema/user.ts b/src/data/schema/user.ts index 6b4829fb7..19ecc8887 100644 --- a/src/data/schema/user.ts +++ b/src/data/schema/user.ts @@ -66,6 +66,7 @@ export const types = ` isOwner: Boolean permissionActions: JSON configs: JSON + lastSeenAt: Date } type UserConversationListResponse { diff --git a/src/db/models/definitions/flowActions.ts b/src/db/models/definitions/flowActions.ts index 25c2ad280..217a98593 100644 --- a/src/db/models/definitions/flowActions.ts +++ b/src/db/models/definitions/flowActions.ts @@ -21,6 +21,7 @@ export interface IFlowActionValueCondition { values: string[]; action: string; value: string; + error: string; } export interface IFlowActionValue { diff --git a/src/db/models/definitions/users.ts b/src/db/models/definitions/users.ts index 66a8e05eb..ea8222670 100644 --- a/src/db/models/definitions/users.ts +++ b/src/db/models/definitions/users.ts @@ -47,6 +47,7 @@ export interface IUser { details?: IDetail; links?: ILink; isActive?: boolean; + lastSeenAt?: number; brandIds?: string[]; groupIds?: string[]; deviceTokens?: string[]; @@ -78,7 +79,11 @@ const detailSchema = new Schema( position: field({ type: String, label: 'Position' }), location: field({ type: String, optional: true, label: 'Location' }), description: field({ type: String, optional: true, label: 'Description' }), - operatorPhone: field({ type: String, optional: true, label: 'Company phone' }), + operatorPhone: field({ + type: String, + optional: true, + label: 'Company phone', + }), }, { _id: false }, ); @@ -99,14 +104,29 @@ export const userSchema = new Schema({ match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,8})+$/, 'Please fill a valid email address'], label: 'Email', }), - getNotificationByEmail: field({ type: Boolean, label: 'Get notification by email' }), - emailSignatures: field({ type: [emailSignatureSchema], label: 'Email signatures' }), - starredConversationIds: field({ type: [String], label: 'Starred conversations' }), + getNotificationByEmail: field({ + type: Boolean, + label: 'Get notification by email', + }), + emailSignatures: field({ + type: [emailSignatureSchema], + label: 'Email signatures', + }), + starredConversationIds: field({ + type: [String], + label: 'Starred conversations', + }), details: field({ type: detailSchema, default: {}, label: 'Details' }), links: field({ type: linkSchema, default: {}, label: 'Links' }), isActive: field({ type: Boolean, default: true, label: 'Is active' }), brandIds: field({ type: [String], label: 'Brands' }), groupIds: field({ type: [String], label: 'Groups' }), deviceTokens: field({ type: [String], default: [], label: 'Device tokens' }), - doNotDisturb: field({ type: String, optional: true, default: 'No', label: 'Do not disturb' }), + doNotDisturb: field({ + type: String, + optional: true, + default: 'No', + label: 'Do not disturb', + }), + lastSeenAt: field({ type: Date, label: 'Last seen at', optional: true }), }); From af76b10b47f44acc1c2ed8ce5ddddb1425b1a6ec Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 17 Apr 2020 09:38:13 -0300 Subject: [PATCH 12/71] improved flow --- src/commands/initProject.ts | 96 ++++++------ src/data/modules/flow/index.ts | 261 +++++++++++++++++++-------------- 2 files changed, 199 insertions(+), 158 deletions(-) diff --git a/src/commands/initProject.ts b/src/commands/initProject.ts index 39c4e876b..8f76c26f4 100644 --- a/src/commands/initProject.ts +++ b/src/commands/initProject.ts @@ -81,7 +81,7 @@ connect() if (!rootFlow) { rootFlow = await Flows.createFlow({ - assignedUserId: 'wJjxQipSXXHbNyqFZ', + assignedUserId: 'XenYe8QirmuiMdCoM', name: 'Root', description: '', }); @@ -91,7 +91,7 @@ connect() if (!secondFlow) { secondFlow = await Flows.createFlow({ - assignedUserId: 'wJjxQipSXXHbNyqFZ', + assignedUserId: 'XenYe8QirmuiMdCoM', name: 'Second', description: '', }); @@ -101,8 +101,8 @@ connect() type: 'erxes.action.to.ask', }); - const sendMessageType = await FlowActionTypes.findOne({ - type: 'erxes.action.send.message', + const transferToAgentType = await FlowActionTypes.findOne({ + type: 'erxes.action.transfer.to.agent', }); const defineDepartmenType = await FlowActionTypes.findOne({ @@ -121,22 +121,24 @@ connect() actionId: askType?.id, value: JSON.stringify({ content: [ - 'Olá, tudo bem?' + - '
Muito bom vê-lo aqui! 😊
' + - '
Bom, para agilizar seu atendimento, por favor digite a opção desejada:' + - '
1 - Ainda não sou cliente, quero falar com o Comercial.' + - '
2 - Já sou cliente, quero falar com o Suporte pois tenho dúvidas ou preciso de algo', + 'Olá, tudo bem?
' + + '
Eu sou a Duda, assistente virtual da DUES! 😃
' + + '
Estou aqui para ajudá-lo a ter um atendimento rápido e objetivo
' + + '
Vamos começar?
' + + '
Primeiro selecione a opção desejada digitando 1 ou 2🤝
' + + '
1. JÁ SOU CLIENTE - Preciso de ajuda ou tirar dúvidas' + + '
2. NÃO SOU CLIENTE - Quero saber mais informações sobre formalização de trabalho autônomo através do MEI e ou sobre os serviços da DUES para MEI', ], conditions: [ { operator: '=', - values: ['1', 'comercial', 'vendas', 'venda'], + values: ['1', 'suporte', 'ajuda', 'duvidas'], action: 'erxes.action.execute.action', value: '1', }, { operator: '=', - values: ['2', 'suporte'], + values: ['2', 'comercial', 'vendas', 'venda', 'formalização', 'mei'], action: 'erxes.action.execute.action', value: '3', }, @@ -149,7 +151,7 @@ connect() flowId: rootFlow.id, type: defineDepartmenType?.type, actionId: defineDepartmenType?.id, - value: 'Rs8GERDMd4PK5xnKv', + value: 'ikWdCxM9Dmr8nMCMy', }); await FlowActions.createFlowAction({ @@ -165,15 +167,25 @@ connect() flowId: rootFlow.id, type: defineDepartmenType?.type, actionId: defineDepartmenType?.id, - value: 'RybHspzXFcc2GPtG4', + value: 'HDDNCHv2fyEghrFYA', }); await FlowActions.createFlowAction({ order: 4, flowId: rootFlow.id, - type: executeFlowType?.type, - actionId: executeFlowType?.id, - value: secondFlow.id, + type: transferToAgentType?.type, + actionId: transferToAgentType?.id, + value: JSON.stringify({ + value: 'Você está sendo transferido para o atendente {{shortName}}', + error: + 'Ops!! 😊

' + + 'Neste momento estamos com todos os nossos atendentes ocupados.

' + + 'Mas registre aqui (mensagem ou áudio) o que precisa, que no máximo em 3 horas iremos responder você.

' + + 'Lembrando que nosso horário de atendimento é das 09:00 às 18:00, de segunda a sexta, exceto feriados.

' + + 'Agradecemos seu contato, ele é muito importante pra nós.

' + + 'Até breve!
' + + 'Equipe Dues', + }), }); } @@ -186,49 +198,33 @@ connect() value: JSON.stringify({ content: [ '
Legal, por favor digite a opção desejada:' + - '
1 - Informações sobre MEI e nossos serviços' + - '
2 - Falar com um atendente', + '
1. Informações sobre minha MEI (Impostos, Alterações, Guias, etc...)' + + '
2. Preciso de Declaração de Renda' + + '
3. Informações sobre o Seguro de Vida' + + '
4. Informações sobre Imposto de Renda de Pessoa Física (IRPF)' + + '
5. Suporte para acesso a financiamentos' + + '
6. Suporte para acesso a planos de saúde , odontológicos e seguros' + + '
7. Suporte para acesso a serviços do INSS (esta doente ou se acidentou)' + + '
8. Nenhum dos assuntos acima, quero falar com um atendente', ], conditions: [ { operator: '=', - values: [ - '1', - 'mei', - 'servicos', - 'serviços', - 'servico', - 'serviço', - 'info', - 'informações', - 'informacoes', - 'infos', - 'sobre', - ], - action: 'erxes.action.execute.action', - value: '1', - }, - { - operator: '=', - values: ['2', 'falar', 'atendente', 'atendimento'], + values: ['1', '2', '3', '4', '5', '6', '7', '8', 'falar', 'atendente', 'atendimento'], action: 'erxes.action.transfer.to.agent', - value: '', + value: 'Você está sendo transferido para o atendente {{shortName}}', + error: + 'Ops!! 😊

' + + 'Neste momento estamos com todos os nossos atendentes ocupados.

' + + 'Mas registre aqui (mensagem ou áudio) o que precisa, que no máximo em 3 horas iremos responder você.

' + + 'Lembrando que nosso horário de atendimento é das 09:00 às 18:00, de segunda a sexta, exceto feriados.

' + + 'Agradecemos seu contato, ele é muito importante pra nós.

' + + 'Até breve!
' + + 'Equipe Dues', }, ], }), }); - - await FlowActions.createFlowAction({ - order: 1, - flowId: secondFlow.id, - type: sendMessageType?.type, - actionId: sendMessageType?.id, - value: JSON.stringify({ - content: [ - 'Para saber mais sobre MEI e nossos serviços acesse:
' + 'https://dues.gpages.com.br/pagina-captura', - ], - }), - }); } }) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 8485061e0..83b338927 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -145,114 +145,7 @@ const handleMessage = async (msg: IMessageDocument) => { break; case 'erxes.action.transfer.to.agent': - let assignedUserId: string = ''; - - let channel = await Channels.findById(conversation.channelId); - - if (channel && channel.memberIds?.length) { - let users = await Users.aggregate([ - { - $match: { - _id: { $in: channel.memberIds }, - lastSeenAt: { - $gte: moment() - .subtract(61, 'seconds') - .toDate(), - }, - }, - }, - { $sample: { size: 1 } }, - ]); - - if (users?.length) { - assignedUserId = users[0]._id; - } - } - - if (!assignedUserId) { - let users = await Users.aggregate([ - { - $match: { - brandIds: { $in: [integration.brandId] }, - lastSeenAt: { - $gte: moment() - .subtract(61, 'seconds') - .toDate(), - }, - }, - }, - { $sample: { size: 1 } }, - ]); - - if (users?.length) { - assignedUserId = users[0]._id; - } - } - - if (assignedUserId) { - const conversations: IConversationDocument[] = await Conversations.assignUserConversation( - [conversation.id], - assignedUserId, - ); - - // notify graphl subscription - publishConversationsChanged([conversation.id], 'assigneeChanged'); - - let assignedUser = await Users.getUser(assignedUserId); - - for (const conversation of conversations) { - let message = await ConversationMessages.addMessage({ - conversationId: conversation._id, - content: `${assignedUser.details?.shortName || - assignedUser.email} has been assigned to this conversation`, - fromBot: true, - }); - - graphqlPubsub.publish('conversationClientMessageInserted', { - conversationClientMessageInserted: message, - }); - - if (condition.value) { - let content = condition.value; - let details: IDetail = assignedUser.details?.toObject() || {}; - - const keys = Object.keys(details); - - for (const key of keys) { - content = content.replace(new RegExp(`{{${key}}}`), details[key]); - } - - handleSendMessage( - integration, - conversation, - { - conversationId: conversation.id, - flowActionId: flowAction.id, - internal: false, - content, - }, - assignedUser, - ); - } - } - } else if (condition.error) { - const user = await Users.findById(flow?.assignedUserId); - - if (user) { - handleSendMessage( - integration, - conversation, - { - conversationId: conversation.id, - flowActionId: flowAction.id, - internal: false, - content: condition.error, - }, - user, - ); - } - } - + await handleTransferToAgent(flowAction, conversation, integration, condition); return; default: break; @@ -312,6 +205,12 @@ const handleMessage = async (msg: IMessageDocument) => { ) sendNextMessage = true; + break; + case 'erxes.action.transfer.to.agent': + const condition = JSON.parse(flowAction.value || '{}'); + + handleTransferToAgent(flowAction, conversation, integration, condition); + break; case 'erxes.action.execute.autmations.flow': sendNextMessage = true; @@ -327,6 +226,152 @@ const handleMessage = async (msg: IMessageDocument) => { } }; +const handleTransferToAgent = async ( + flowAction: IFlowActionDocument, + conversation: IConversationDocument, + integration: IIntegrationDocument, + condition: IFlowActionValueCondition, +) => { + let assignedUserId: string = ''; + + let channel = await Channels.findById(conversation.channelId); + + if (channel && channel.memberIds?.length) { + let users = await Users.aggregate([ + { + $match: { + _id: { $in: channel.memberIds }, + lastSeenAt: { + $gte: moment() + .subtract(61, 'seconds') + .toDate(), + }, + }, + }, + { $sample: { size: 1 } }, + ]); + + if (users?.length) { + assignedUserId = users[0]._id; + } + } + + if (assignedUserId) { + const conversations: IConversationDocument[] = await Conversations.assignUserConversation( + [conversation.id], + assignedUserId, + ); + + // notify graphl subscription + publishConversationsChanged([conversation.id], 'assigneeChanged'); + + let assignedUser = await Users.getUser(assignedUserId); + + for (const conversation of conversations) { + let message = await ConversationMessages.addMessage({ + conversationId: conversation._id, + content: `${assignedUser.details?.shortName || assignedUser.email} has been assigned to this conversation`, + fromBot: true, + }); + + graphqlPubsub.publish('conversationClientMessageInserted', { + conversationClientMessageInserted: message, + }); + + if (condition.value) { + let content = condition.value; + let details: IDetail = assignedUser.details?.toObject() || {}; + + const keys = Object.keys(details); + + for (const key of keys) { + content = content.replace(new RegExp(`{{${key}}}`), details[key]); + } + + handleSendMessage( + integration, + conversation, + { + conversationId: conversation.id, + flowActionId: flowAction.id, + internal: false, + content, + }, + assignedUser, + ); + } + } + } else if (condition.error) { + let user: IUserDocument | null = null; + + if (channel && channel.memberIds?.length) { + let users = await Users.aggregate([ + { + $match: { + brandIds: { $in: [integration.brandId] }, + }, + }, + { $sample: { size: 1 } }, + ]); + + if (users?.length) { + user = users[0]; + } + } + + if (!user) { + let users = await Users.aggregate([ + { + $match: { + brandIds: { $in: [integration.brandId] }, + }, + }, + { $sample: { size: 1 } }, + ]); + + if (users?.length) { + user = users[0]; + } + } + + if (!user) user = await Users.findOne({ isOwner: true }); + + if (user) { + const conversations: IConversationDocument[] = await Conversations.assignUserConversation( + [conversation.id], + user._id, + ); + + // notify graphl subscription + publishConversationsChanged([conversation.id], 'assigneeChanged'); + + for (const conversation of conversations) { + let message = await ConversationMessages.addMessage({ + conversationId: conversation._id, + content: `${user.details?.shortName || user.email} has been assigned to this conversation`, + fromBot: true, + }); + + graphqlPubsub.publish('conversationClientMessageInserted', { + conversationClientMessageInserted: message, + }); + + handleSendMessage( + integration, + conversation, + { + conversationId: conversation.id, + flowActionId: flowAction.id, + internal: false, + content: condition.error, + }, + user, + ); + } + } + } +}; + const proccessSendMessage = async (flowAction: IFlowActionDocument, conversation: IConversationDocument) => { const integration = await Integrations.getIntegration(conversation.integrationId); From 7e5f8618fb6fc3fe18bea792014e690156292e6a Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Wed, 22 Apr 2020 19:29:37 -0300 Subject: [PATCH 13/71] fix brand issue --- src/apolloClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apolloClient.ts b/src/apolloClient.ts index 8ce264876..50e3617af 100644 --- a/src/apolloClient.ts +++ b/src/apolloClient.ts @@ -104,7 +104,7 @@ const apolloServer = new ApolloServer({ return { brandIdSelector, - docModifier: doc => ({ ...doc, scopeBrandIds }), + docModifier: doc => ({ ...doc, scopeBrandIds: scopeBrandIds.filter((c: any) => c?.length) }), commonQuerySelector, commonQuerySelectorElk, userBrandIdsSelector, From d6e829d52a564f417387fb6db862c2cec36ad27b Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sun, 3 May 2020 19:10:57 -0300 Subject: [PATCH 14/71] add condition on onboarding active --- src/commands/initProject.ts | 105 ++++++++++++++++------- src/data/modules/flow/index.ts | 50 +++++++++-- src/db/models/definitions/flowActions.ts | 2 + 3 files changed, 122 insertions(+), 35 deletions(-) diff --git a/src/commands/initProject.ts b/src/commands/initProject.ts index 8f76c26f4..f8d5e62fd 100644 --- a/src/commands/initProject.ts +++ b/src/commands/initProject.ts @@ -97,6 +97,14 @@ connect() }); } + const conditionType = await FlowActionTypes.findOne({ + type: 'erxes.action.conditional', + }); + + const sendMessageType = await FlowActionTypes.findOne({ + type: 'erxes.action.send.message', + }); + const askType = await FlowActionTypes.findOne({ type: 'erxes.action.to.ask', }); @@ -116,6 +124,27 @@ connect() if (!(await FlowActions.count({ flowId: rootFlow.id }))) { await FlowActions.createFlowAction({ order: 0, + type: conditionType?.type, + flowId: rootFlow.id, + actionId: conditionType?.id, + value: JSON.stringify({ + conditions: [ + { + type: 'erxes.conditional.variable', + operator: '=', + variable: { + key: 'onboarding_active', + value: '0', + }, + action: 'erxes.action.execute.action', + value: '6', + }, + ], + }), + }); + + await FlowActions.createFlowAction({ + order: 1, type: askType?.type, flowId: rootFlow.id, actionId: askType?.id, @@ -123,31 +152,31 @@ connect() content: [ 'Olá, tudo bem?
' + '
Eu sou a Duda, assistente virtual da DUES! 😃
' + - '
Estou aqui para ajudá-lo a ter um atendimento rápido e objetivo
' + + '
Estou aqui para ajudá-lo a ter um atendimento mais rápido e objetivo.
' + '
Vamos começar?
' + - '
Primeiro selecione a opção desejada digitando 1 ou 2🤝
' + - '
1. JÁ SOU CLIENTE - Preciso de ajuda ou tirar dúvidas' + - '
2. NÃO SOU CLIENTE - Quero saber mais informações sobre formalização de trabalho autônomo através do MEI e ou sobre os serviços da DUES para MEI', + '
Primeiro selecione a opção desejada digitando 1 ou 2.🤝
' + + '
1. JÁ SOU CLIENTE - Preciso de ajuda ou tirar dúvidas.' + + '
2. NÃO SOU CLIENTE - Quero saber mais informações sobre formalização de trabalho autônomo através do MEI e ou sobre os serviços da DUES para MEI', ], conditions: [ { operator: '=', values: ['1', 'suporte', 'ajuda', 'duvidas'], action: 'erxes.action.execute.action', - value: '1', + value: '2', }, { operator: '=', values: ['2', 'comercial', 'vendas', 'venda', 'formalização', 'mei'], action: 'erxes.action.execute.action', - value: '3', + value: '4', }, ], }), }); await FlowActions.createFlowAction({ - order: 1, + order: 2, flowId: rootFlow.id, type: defineDepartmenType?.type, actionId: defineDepartmenType?.id, @@ -155,7 +184,7 @@ connect() }); await FlowActions.createFlowAction({ - order: 2, + order: 3, flowId: rootFlow.id, type: executeFlowType?.type, actionId: executeFlowType?.id, @@ -163,7 +192,7 @@ connect() }); await FlowActions.createFlowAction({ - order: 3, + order: 4, flowId: rootFlow.id, type: defineDepartmenType?.type, actionId: defineDepartmenType?.id, @@ -171,22 +200,38 @@ connect() }); await FlowActions.createFlowAction({ - order: 4, + order: 5, flowId: rootFlow.id, type: transferToAgentType?.type, actionId: transferToAgentType?.id, value: JSON.stringify({ - value: 'Você está sendo transferido para o atendente {{shortName}}', + value: 'Obrigado pelas informações, aguarde um minuto que estamos transferindo você para a(o) {{shortName}}', error: - 'Ops!! 😊

' + + 'Ops!!

' + 'Neste momento estamos com todos os nossos atendentes ocupados.

' + - 'Mas registre aqui (mensagem ou áudio) o que precisa, que no máximo em 3 horas iremos responder você.

' + - 'Lembrando que nosso horário de atendimento é das 09:00 às 18:00, de segunda a sexta, exceto feriados.

' + - 'Agradecemos seu contato, ele é muito importante pra nós.

' + - 'Até breve!
' + + 'Mas registre aqui o que precisa por mensagem ou áudio, que o responderemos o mais rápido possível, no máximo em 3 horas, mas normalmente antes.

' + + 'Agradecemos seu contato, ele é muito importante para nós.

' + + 'Até breve! 😊
' + 'Equipe Dues', }), }); + + await FlowActions.createFlowAction({ + order: 6, + flowId: rootFlow.id, + type: sendMessageType?.type, + actionId: sendMessageType?.id, + value: JSON.stringify({ + content: [ + 'Olá, tudo bem?

' + + 'Bom, neste momento não estamos ONLINE pois o nosso horário de atendimento é das 09:00 às 18:00, exceto sábados, domingos e feriados.

' + + 'Mas fique tranquilo, deixe aqui sua mensagem de texto ou de voz que assim que retornarmos o responderemos.

' + + 'Lembramos que nosso atendimento é somente ONLINE, ou seja, somente através de mensagens por aplicativos ou e-mails, mas caso necessário e em situações pontuais, nós ligaremos para você.

' + + 'Até breve! 😊
' + + 'Equipe Dues', + ], + }), + }); } if (!(await FlowActions.count({ flowId: secondFlow.id }))) { @@ -198,28 +243,28 @@ connect() value: JSON.stringify({ content: [ '
Legal, por favor digite a opção desejada:' + - '
1. Informações sobre minha MEI (Impostos, Alterações, Guias, etc...)' + - '
2. Preciso de Declaração de Renda' + - '
3. Informações sobre o Seguro de Vida' + - '
4. Informações sobre Imposto de Renda de Pessoa Física (IRPF)' + - '
5. Suporte para acesso a financiamentos' + - '
6. Suporte para acesso a planos de saúde , odontológicos e seguros' + - '
7. Suporte para acesso a serviços do INSS (esta doente ou se acidentou)' + - '
8. Nenhum dos assuntos acima, quero falar com um atendente', + '
1. Minha MEI (Impostos, Alterações, Guias etc.);' + + '
2. Declaração de Renda;' + + '
3. Seguro de Vida;' + + '
4. Imposto de Renda de Pessoa Física (IRPF);' + + '
5. Suporte para acesso a Financiamentos;' + + '
6. Suporte para acesso a Planos de Saúde , Odontológicos e Seguros;' + + '
7. Suporte para acesso a serviços do INSS (esta doente ou se acidentou);' + + '
8. Nenhum dos assuntos acima;', ], conditions: [ { operator: '=', values: ['1', '2', '3', '4', '5', '6', '7', '8', 'falar', 'atendente', 'atendimento'], action: 'erxes.action.transfer.to.agent', - value: 'Você está sendo transferido para o atendente {{shortName}}', + value: + 'Obrigado pelas informações, aguarde um minuto que estamos transferindo você para a(o) {{shortName}}', error: - 'Ops!! 😊

' + + 'Ops!!

' + 'Neste momento estamos com todos os nossos atendentes ocupados.

' + - 'Mas registre aqui (mensagem ou áudio) o que precisa, que no máximo em 3 horas iremos responder você.

' + - 'Lembrando que nosso horário de atendimento é das 09:00 às 18:00, de segunda a sexta, exceto feriados.

' + - 'Agradecemos seu contato, ele é muito importante pra nós.

' + - 'Até breve!
' + + 'Mas registre aqui o que precisa por mensagem ou áudio, que o responderemos o mais rápido possível, no máximo em 3 horas, mas normalmente antes.

' + + 'Agradecemos seu contato, ele é muito importante para nós.

' + + 'Até breve! 😊
' + 'Equipe Dues', }, ], diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 83b338927..eb5809063 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -28,12 +28,26 @@ import Messages from '../../../db/models/ConversationMessages'; import { IIntegrationDocument } from '../../../db/models/definitions/integrations'; import { IDetail, IUserDocument } from '../../../db/models/definitions/users'; -const actionWithSendNext = ['erxes.action.send.message', 'erxes.action.define.department']; +const actionWithSendNext = ['erxes.action.send.message', 'erxes.action.define.department', 'erxes.action.conditional']; -const handleCondition = (condition: IFlowActionValueCondition, content: string = '') => { +const checkIfIsCondition = (condition: IFlowActionValueCondition, content: string = '') => { switch (condition.operator) { case '=': - return condition.values.includes(content); + switch (condition.type) { + case 'erxes.conditional.variable': + switch (condition.variable.key) { + case 'onboarding_active': + let not = [0, 6].includes(moment().weekday()) || moment().hour() < 9 || moment().hour() > 18; + return (condition.variable.value === '0' && not) || (condition.variable.value === '1' && !not); + + default: + break; + } + break; + + default: + return condition.values.includes(content); + } default: return false; } @@ -130,10 +144,10 @@ const handleMessage = async (msg: IMessageDocument) => { currentFlowActionId: flowAction?.id, }); break; - case 'erxes.action.to.ask': + case 'erxes.action.to.ask': { const { conditions }: IFlowActionValue = JSON.parse(flowAction.value || '{}'); - const condition = conditions.find(c => handleCondition(c, msg.content)); + const condition = conditions.find(c => checkIfIsCondition(c, msg.content)); if (condition) { switch (condition.action) { @@ -152,6 +166,32 @@ const handleMessage = async (msg: IMessageDocument) => { } } break; + } + case 'erxes.action.conditional': { + const { conditions }: IFlowActionValue = JSON.parse(flowAction.value || '{}'); + + const condition = conditions.find(c => checkIfIsCondition(c, msg.content)); + + if (condition) { + switch (condition.action) { + case 'erxes.action.execute.action': + flowAction = await FlowActions.findOne({ + flowId: flowAction.flowId, + order: condition.value, + }); + + break; + default: + break; + } + } else { + flowAction = await FlowActions.findOne({ + flowId: flowAction.flowId, + order: flowAction.order + 1, + }); + } + break; + } default: break; } diff --git a/src/db/models/definitions/flowActions.ts b/src/db/models/definitions/flowActions.ts index 217a98593..fdef16ff1 100644 --- a/src/db/models/definitions/flowActions.ts +++ b/src/db/models/definitions/flowActions.ts @@ -18,9 +18,11 @@ export interface IFlowActionDocument extends IFlowAction, Document { export interface IFlowActionValueCondition { operator: string; + type: string; values: string[]; action: string; value: string; + variable: any; error: string; } From c60f3c1fa625d5a1b2335f38900f46268bc4cfa6 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Mon, 4 May 2020 19:04:05 -0300 Subject: [PATCH 15/71] fix utc time --- src/data/modules/flow/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index eb5809063..99e91356b 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -37,7 +37,9 @@ const checkIfIsCondition = (condition: IFlowActionValueCondition, content: strin case 'erxes.conditional.variable': switch (condition.variable.key) { case 'onboarding_active': - let not = [0, 6].includes(moment().weekday()) || moment().hour() < 9 || moment().hour() > 18; + let now = moment.utc(); + now = now.utcOffset(-180); + let not = [0, 6].includes(now.weekday()) || now.hour() < 9 || now.hour() > 18; return (condition.variable.value === '0' && not) || (condition.variable.value === '1' && !not); default: From bb77c61d9b382a5d8bdf81abb3c55dd9d3d565da Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Tue, 5 May 2020 20:06:17 -0300 Subject: [PATCH 16/71] fix elk index lowercase --- elkSyncer/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elkSyncer/main.py b/elkSyncer/main.py index 49904073e..23e70f773 100644 --- a/elkSyncer/main.py +++ b/elkSyncer/main.py @@ -5,7 +5,7 @@ from dotenv import load_dotenv from elasticsearch import Elasticsearch import pymongo - +print('test') load_dotenv() MONGO_URL = os.getenv('MONGO_URL') @@ -137,7 +137,7 @@ def put_mappings(index, mapping): print(e) -db_name = pymongo.uri_parser.parse_uri(MONGO_URL)['database'] +db_name = pymongo.uri_parser.parse_uri(MONGO_URL)['database'].lower() put_mappings('%s__customers' % db_name, customer_mapping) put_mappings('%s__companies' % db_name, company_mapping) From 6c231bf08c089b2416bd9a51d0db8138c2ab40d7 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Tue, 5 May 2020 20:12:10 -0300 Subject: [PATCH 17/71] fix elk issue lowercase --- elkSyncer/main.py | 2 +- src/elasticsearch.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/elkSyncer/main.py b/elkSyncer/main.py index 23e70f773..9e0524aa0 100644 --- a/elkSyncer/main.py +++ b/elkSyncer/main.py @@ -5,7 +5,7 @@ from dotenv import load_dotenv from elasticsearch import Elasticsearch import pymongo -print('test') + load_dotenv() MONGO_URL = os.getenv('MONGO_URL') diff --git a/src/elasticsearch.ts b/src/elasticsearch.ts index 3d3023993..8cb8cbe73 100644 --- a/src/elasticsearch.ts +++ b/src/elasticsearch.ts @@ -18,7 +18,7 @@ export const getMappings = async (index: string) => { export const getIndexPrefix = () => { const uriObject = mongoUri.parse(MONGO_URL); - const dbName = uriObject.database; + const dbName = uriObject.database.toLowerCase(); return `${dbName}__`; }; From e1564eacf4a81eab6b1bac0e4c227a5e0263a61d Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 7 May 2020 08:42:38 -0300 Subject: [PATCH 18/71] refactor --- src/commands/initFlow.ts | 53 ++++++++ src/commands/initProject.ts | 241 +----------------------------------- 2 files changed, 54 insertions(+), 240 deletions(-) create mode 100644 src/commands/initFlow.ts diff --git a/src/commands/initFlow.ts b/src/commands/initFlow.ts new file mode 100644 index 000000000..bad9a2583 --- /dev/null +++ b/src/commands/initFlow.ts @@ -0,0 +1,53 @@ +import { connect, disconnect } from '../db/connection'; +import { FlowActionTypes } from '../db/models'; + +connect() + .then(async () => { + let flowActionTypes = await FlowActionTypes.find({}); + + if (!flowActionTypes || !flowActionTypes.length) { + flowActionTypes = [ + 'erxes.action.root', + 'erxes.action.define.department', + 'erxes.action.define.tabulation', + 'erxes.action.transfer.to.agent', + 'erxes.action.put.in.queue', + 'erxes.action.finish.attendance', + 'erxes.action.auto.distribute', + 'erxes.action.define.bot', + 'erxes.action.define.tags', + 'erxes.action.add.tags', + 'erxes.action.send.message', + 'erxes.action.send.template.message', + 'erxes.action.dispatch.to.app', + 'erxes.action.send.sms', + 'erxes.action.define.timeout', + 'erxes.action.send.internal.message', + 'erxes.action.execute.autmations.flow', + 'erxes.action.execute.action', + 'erxes.action.conditional', + 'erxes.action.wait.for.interaction', + 'erxes.action.define.workflow', + 'erxes.action.send.feedback', + 'erxes.action.add.comment.timeline', + 'erxes.action.choose.department', + 'erxes.action.to.ask', + 'erxes.action.send.request', + 'erxes.action.send.email', + 'erxes.action.send.file', + 'erxes.action.execute.javascript', + 'erxes.action.define.virtual.agent', + 'erxes.action.pause', + ].map(type => new FlowActionTypes({ type, createdAt: new Date() })); + + return FlowActionTypes.insertMany(flowActionTypes); + } + }) + + .then(() => { + return disconnect(); + }) + + .then(() => { + process.exit(); + }); diff --git a/src/commands/initProject.ts b/src/commands/initProject.ts index f8d5e62fd..699fc1496 100644 --- a/src/commands/initProject.ts +++ b/src/commands/initProject.ts @@ -1,5 +1,5 @@ import { connect, disconnect } from '../db/connection'; -import { Users, FlowActionTypes, Flows, FlowActions } from '../db/models'; +import { Users } from '../db/models'; connect() .then(async () => { @@ -34,245 +34,6 @@ connect() return; }) - .then(async () => { - let flowActionTypes = await FlowActionTypes.find({}); - - if (!flowActionTypes || !flowActionTypes.length) { - flowActionTypes = [ - 'erxes.action.root', - 'erxes.action.define.department', - 'erxes.action.define.tabulation', - 'erxes.action.transfer.to.agent', - 'erxes.action.put.in.queue', - 'erxes.action.finish.attendance', - 'erxes.action.auto.distribute', - 'erxes.action.define.bot', - 'erxes.action.define.tags', - 'erxes.action.add.tags', - 'erxes.action.send.message', - 'erxes.action.send.template.message', - 'erxes.action.dispatch.to.app', - 'erxes.action.send.sms', - 'erxes.action.define.timeout', - 'erxes.action.send.internal.message', - 'erxes.action.execute.autmations.flow', - 'erxes.action.execute.action', - 'erxes.action.conditional', - 'erxes.action.wait.for.interaction', - 'erxes.action.define.workflow', - 'erxes.action.send.feedback', - 'erxes.action.add.comment.timeline', - 'erxes.action.choose.department', - 'erxes.action.to.ask', - 'erxes.action.send.request', - 'erxes.action.send.email', - 'erxes.action.send.file', - 'erxes.action.execute.javascript', - 'erxes.action.define.virtual.agent', - 'erxes.action.pause', - ].map(type => new FlowActionTypes({ type, createdAt: new Date() })); - - return FlowActionTypes.insertMany(flowActionTypes); - } - }) - - .then(async () => { - let rootFlow = await Flows.findOne({ name: 'Root' }); - - if (!rootFlow) { - rootFlow = await Flows.createFlow({ - assignedUserId: 'XenYe8QirmuiMdCoM', - name: 'Root', - description: '', - }); - } - - let secondFlow = await Flows.findOne({ name: 'Second' }); - - if (!secondFlow) { - secondFlow = await Flows.createFlow({ - assignedUserId: 'XenYe8QirmuiMdCoM', - name: 'Second', - description: '', - }); - } - - const conditionType = await FlowActionTypes.findOne({ - type: 'erxes.action.conditional', - }); - - const sendMessageType = await FlowActionTypes.findOne({ - type: 'erxes.action.send.message', - }); - - const askType = await FlowActionTypes.findOne({ - type: 'erxes.action.to.ask', - }); - - const transferToAgentType = await FlowActionTypes.findOne({ - type: 'erxes.action.transfer.to.agent', - }); - - const defineDepartmenType = await FlowActionTypes.findOne({ - type: 'erxes.action.define.department', - }); - - const executeFlowType = await FlowActionTypes.findOne({ - type: 'erxes.action.execute.autmations.flow', - }); - - if (!(await FlowActions.count({ flowId: rootFlow.id }))) { - await FlowActions.createFlowAction({ - order: 0, - type: conditionType?.type, - flowId: rootFlow.id, - actionId: conditionType?.id, - value: JSON.stringify({ - conditions: [ - { - type: 'erxes.conditional.variable', - operator: '=', - variable: { - key: 'onboarding_active', - value: '0', - }, - action: 'erxes.action.execute.action', - value: '6', - }, - ], - }), - }); - - await FlowActions.createFlowAction({ - order: 1, - type: askType?.type, - flowId: rootFlow.id, - actionId: askType?.id, - value: JSON.stringify({ - content: [ - 'Olá, tudo bem?
' + - '
Eu sou a Duda, assistente virtual da DUES! 😃
' + - '
Estou aqui para ajudá-lo a ter um atendimento mais rápido e objetivo.
' + - '
Vamos começar?
' + - '
Primeiro selecione a opção desejada digitando 1 ou 2.🤝
' + - '
1. JÁ SOU CLIENTE - Preciso de ajuda ou tirar dúvidas.' + - '
2. NÃO SOU CLIENTE - Quero saber mais informações sobre formalização de trabalho autônomo através do MEI e ou sobre os serviços da DUES para MEI', - ], - conditions: [ - { - operator: '=', - values: ['1', 'suporte', 'ajuda', 'duvidas'], - action: 'erxes.action.execute.action', - value: '2', - }, - { - operator: '=', - values: ['2', 'comercial', 'vendas', 'venda', 'formalização', 'mei'], - action: 'erxes.action.execute.action', - value: '4', - }, - ], - }), - }); - - await FlowActions.createFlowAction({ - order: 2, - flowId: rootFlow.id, - type: defineDepartmenType?.type, - actionId: defineDepartmenType?.id, - value: 'ikWdCxM9Dmr8nMCMy', - }); - - await FlowActions.createFlowAction({ - order: 3, - flowId: rootFlow.id, - type: executeFlowType?.type, - actionId: executeFlowType?.id, - value: secondFlow.id, - }); - - await FlowActions.createFlowAction({ - order: 4, - flowId: rootFlow.id, - type: defineDepartmenType?.type, - actionId: defineDepartmenType?.id, - value: 'HDDNCHv2fyEghrFYA', - }); - - await FlowActions.createFlowAction({ - order: 5, - flowId: rootFlow.id, - type: transferToAgentType?.type, - actionId: transferToAgentType?.id, - value: JSON.stringify({ - value: 'Obrigado pelas informações, aguarde um minuto que estamos transferindo você para a(o) {{shortName}}', - error: - 'Ops!!

' + - 'Neste momento estamos com todos os nossos atendentes ocupados.

' + - 'Mas registre aqui o que precisa por mensagem ou áudio, que o responderemos o mais rápido possível, no máximo em 3 horas, mas normalmente antes.

' + - 'Agradecemos seu contato, ele é muito importante para nós.

' + - 'Até breve! 😊
' + - 'Equipe Dues', - }), - }); - - await FlowActions.createFlowAction({ - order: 6, - flowId: rootFlow.id, - type: sendMessageType?.type, - actionId: sendMessageType?.id, - value: JSON.stringify({ - content: [ - 'Olá, tudo bem?

' + - 'Bom, neste momento não estamos ONLINE pois o nosso horário de atendimento é das 09:00 às 18:00, exceto sábados, domingos e feriados.

' + - 'Mas fique tranquilo, deixe aqui sua mensagem de texto ou de voz que assim que retornarmos o responderemos.

' + - 'Lembramos que nosso atendimento é somente ONLINE, ou seja, somente através de mensagens por aplicativos ou e-mails, mas caso necessário e em situações pontuais, nós ligaremos para você.

' + - 'Até breve! 😊
' + - 'Equipe Dues', - ], - }), - }); - } - - if (!(await FlowActions.count({ flowId: secondFlow.id }))) { - await FlowActions.createFlowAction({ - order: 0, - flowId: secondFlow.id, - type: askType?.type, - actionId: askType?.id, - value: JSON.stringify({ - content: [ - '
Legal, por favor digite a opção desejada:' + - '
1. Minha MEI (Impostos, Alterações, Guias etc.);' + - '
2. Declaração de Renda;' + - '
3. Seguro de Vida;' + - '
4. Imposto de Renda de Pessoa Física (IRPF);' + - '
5. Suporte para acesso a Financiamentos;' + - '
6. Suporte para acesso a Planos de Saúde , Odontológicos e Seguros;' + - '
7. Suporte para acesso a serviços do INSS (esta doente ou se acidentou);' + - '
8. Nenhum dos assuntos acima;', - ], - conditions: [ - { - operator: '=', - values: ['1', '2', '3', '4', '5', '6', '7', '8', 'falar', 'atendente', 'atendimento'], - action: 'erxes.action.transfer.to.agent', - value: - 'Obrigado pelas informações, aguarde um minuto que estamos transferindo você para a(o) {{shortName}}', - error: - 'Ops!!

' + - 'Neste momento estamos com todos os nossos atendentes ocupados.

' + - 'Mas registre aqui o que precisa por mensagem ou áudio, que o responderemos o mais rápido possível, no máximo em 3 horas, mas normalmente antes.

' + - 'Agradecemos seu contato, ele é muito importante para nós.

' + - 'Até breve! 😊
' + - 'Equipe Dues', - }, - ], - }), - }); - } - }) - .then(() => { return disconnect(); }) From 4d24358d82b67e822648ecda6762ad7bb1d92010 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 7 May 2020 09:06:13 -0300 Subject: [PATCH 19/71] fix --- src/data/resolvers/mutations/conversations.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/resolvers/mutations/conversations.ts b/src/data/resolvers/mutations/conversations.ts index bd87ddcaf..d920d7f87 100644 --- a/src/data/resolvers/mutations/conversations.ts +++ b/src/data/resolvers/mutations/conversations.ts @@ -273,6 +273,7 @@ const conversationMutations = { // send reply to whatsapp if (kind === KIND_CHOICES.WHATSAPP) { requestName = 'replyWhatsApp'; + } if (kind === KIND_CHOICES.WHATSPRO) { requestName = 'replyWhatsPro'; From 3c9d7ed95359db75cabc627c8108aa9c7356423d Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 7 May 2020 10:20:32 -0300 Subject: [PATCH 20/71] Add WhatsPro Integration Name --- src/db/models/definitions/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/db/models/definitions/constants.ts b/src/db/models/definitions/constants.ts index 1f123f7ef..29593efbe 100644 --- a/src/db/models/definitions/constants.ts +++ b/src/db/models/definitions/constants.ts @@ -117,6 +117,7 @@ export const INTEGRATION_NAMES_MAP = { 'smooch-telegram': 'Telegram', 'smooch-twilio': 'Twilio SMS', whatsapp: 'WhatsApp', + whatspro: 'WhatsPro', }; // messenger data availability constants From 81fb4ed7b4c1a12a5abe00d25fa6ce6205047036 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Tue, 12 May 2020 20:37:46 -0300 Subject: [PATCH 21/71] add createdDate to elksync --- elkSyncer/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/elkSyncer/main.py b/elkSyncer/main.py index c1712fd76..b0bcbe94f 100644 --- a/elkSyncer/main.py +++ b/elkSyncer/main.py @@ -26,6 +26,9 @@ } customer_mapping = { + 'createdAt': { + 'type': 'date', + }, 'state': { 'type': 'keyword', }, @@ -71,6 +74,9 @@ } company_mapping = { + 'createdAt': { + 'type': 'date', + }, 'primaryEmail': { 'type': 'text', 'analyzer': 'uax_url_email_analyzer', From a360594ff5902b72db3ac121276aff083d6add88 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 14 May 2020 16:06:16 -0300 Subject: [PATCH 22/71] change searchText query to search docs that contains the value --- src/data/modules/coc/utils.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/data/modules/coc/utils.ts b/src/data/modules/coc/utils.ts index a7f595ab6..4ec552da5 100644 --- a/src/data/modules/coc/utils.ts +++ b/src/data/modules/coc/utils.ts @@ -153,8 +153,19 @@ export class CommonBuilder { // filter by search value public searchFilter(value: string): void { this.positiveList.push({ - match_phrase: { - searchText: value, + bool: { + should: [ + { + match_phrase: { + searchText: value, + }, + }, + { + wildcard: { + searchText: `*${value}*`, + }, + }, + ], }, }); } From dd25ebfeacbc1d161537ad1652bc34cc14e5aaeb Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 14 May 2020 16:06:32 -0300 Subject: [PATCH 23/71] improved scopeBrandIds elk query --- src/apolloClient.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/apolloClient.ts b/src/apolloClient.ts index 50e3617af..2782ab8c7 100644 --- a/src/apolloClient.ts +++ b/src/apolloClient.ts @@ -97,14 +97,36 @@ const apolloServer = new ApolloServer({ brandIdSelector = { _id: { $in: scopeBrandIds } }; commonQuerySelector = { scopeBrandIds: { $in: scopeBrandIds } }; - commonQuerySelectorElk = { terms: { scopeBrandIds } }; + commonQuerySelectorElk = { + bool: { + should: [ + { + terms: { + scopeBrandIds: scopeBrandIds.filter(c => typeof c === 'string'), + }, + }, + { + bool: { + must_not: { + exists: { + field: 'scopeBrandIds', + }, + }, + }, + }, + ], + }, + }; userBrandIdsSelector = { brandIds: { $in: scopeBrandIds } }; } } return { brandIdSelector, - docModifier: doc => ({ ...doc, scopeBrandIds: scopeBrandIds.filter((c: any) => c?.length) }), + docModifier: doc => ({ + ...doc, + scopeBrandIds: scopeBrandIds.filter((c: any) => c?.length), + }), commonQuerySelector, commonQuerySelectorElk, userBrandIdsSelector, From 456e2400bcd449df3029fb648c5de4fd854c1234 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Tue, 19 May 2020 13:51:00 -0300 Subject: [PATCH 24/71] fix get last activitylogs conversations --- src/data/resolvers/queries/activityLogs.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/data/resolvers/queries/activityLogs.ts b/src/data/resolvers/queries/activityLogs.ts index 2e8b04320..33f16d3c2 100644 --- a/src/data/resolvers/queries/activityLogs.ts +++ b/src/data/resolvers/queries/activityLogs.ts @@ -69,7 +69,9 @@ const activityLogQueries = { const collectConversations = async () => { collectItems( - await Conversations.find({ $or: [{ customerId: contentId }, { participatedUserIds: contentId }] }).limit(25), + await Conversations.find({ $or: [{ customerId: contentId }, { participatedUserIds: contentId }] }) + .sort({ createdAt: -1 }) + .limit(25), 'conversation', ); if (contentType === 'customer') { From 67646f5054feab3f459c5b853dfe72938c75fc1e Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Wed, 20 May 2020 20:29:12 -0300 Subject: [PATCH 25/71] Implementing whatsapp status --- src/data/modules/flow/index.ts | 3 ++ .../modules/integrations/receiveMessage.ts | 30 +++++++++++++++++++ src/data/resolvers/mutations/conversations.ts | 13 +++++++- .../definitions/conversationMessages.ts | 2 ++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 99e91356b..a12efc715 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -505,6 +505,7 @@ const handleSendMessage = async ( doc, { IntegrationsAPI: new IntegrationsAPI() }, action, + message._id, ); const dbMessage = await ConversationMessages.getMessage(message._id); @@ -525,6 +526,7 @@ const sendConversationToIntegrations = ( doc: IConversationMessageAdd, dataSources: any, action?: string, + messageId?: string, ) => { if (type === 'facebook') { return sendMessage('erxes-api:integrations-notification', { @@ -550,6 +552,7 @@ const sendConversationToIntegrations = ( return dataSources.IntegrationsAPI[requestName]({ conversationId, integrationId, + messageId, content: strip(doc.content), attachments: doc.attachments || [], }); diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index 1f88989aa..bd3644669 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -127,6 +127,36 @@ export const receiveRpcMessage = async msg => { return sendSuccess({ _id: message._id }); } + if (action === 'update-conversation-message') { + const message = await ConversationMessages.findById(doc.id); + + if (!message) return sendSuccess({ _id: doc.id }); + + if (!message.status || message.status < doc.status) message.status = doc.status; + + await ConversationMessages.updateOne({ _id: message._id }, { status: message.status, createdAt: doc.createdAt }); + + const conversationDoc: { + updatedAt?: Date; + } = {}; + + if (doc.createdAt) { + conversationDoc.updatedAt = doc.createdAt; + } + + await Conversations.updateConversation(message.conversationId, conversationDoc); + + graphqlPubsub.publish('conversationClientMessageInserted', { + conversationClientMessageInserted: message, + }); + + graphqlPubsub.publish('conversationMessageInserted', { + conversationMessageInserted: message, + }); + + return sendSuccess({ _id: doc.id }); + } + if (action === 'get-configs') { const configs = await getConfigs(); return sendSuccess({ configs }); diff --git a/src/data/resolvers/mutations/conversations.ts b/src/data/resolvers/mutations/conversations.ts index a261cb6bf..9c6b0861e 100644 --- a/src/data/resolvers/mutations/conversations.ts +++ b/src/data/resolvers/mutations/conversations.ts @@ -47,6 +47,7 @@ const sendConversationToIntegrations = ( doc: IConversationMessageAdd, dataSources: any, action?: string, + messageId?: string, ) => { if (type === 'facebook') { return sendMessage('erxes-api:integrations-notification', { @@ -65,6 +66,7 @@ const sendConversationToIntegrations = ( return dataSources.IntegrationsAPI[requestName]({ conversationId, integrationId, + messageId, content: strip(doc.content), attachments: doc.attachments || [], }); @@ -280,7 +282,16 @@ const conversationMutations = { requestName = 'replyWhatsPro'; } - await sendConversationToIntegrations(type, integrationId, conversationId, requestName, doc, dataSources, action); + await sendConversationToIntegrations( + type, + integrationId, + conversationId, + requestName, + doc, + dataSources, + action, + message._id, + ); const dbMessage = await ConversationMessages.getMessage(message._id); diff --git a/src/db/models/definitions/conversationMessages.ts b/src/db/models/definitions/conversationMessages.ts index 31ce00a71..c5e927010 100644 --- a/src/db/models/definitions/conversationMessages.ts +++ b/src/db/models/definitions/conversationMessages.ts @@ -42,6 +42,7 @@ export interface IMessage { engageData?: IEngageData; contentType?: string; flowActionId?: string; + status?: string; } export interface IMessageDocument extends IMessage, Document { @@ -106,4 +107,5 @@ export const messageSchema = new Schema({ default: MESSAGE_TYPES.TEXT, }), flowActionId: field({ type: String, index: true }), + status: field({ type: String }), }); From 86ea3abce0230b0a258a04427a3738fbb2e5fc56 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 22 May 2020 20:50:03 -0300 Subject: [PATCH 26/71] status message --- src/data/schema/conversation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/schema/conversation.ts b/src/data/schema/conversation.ts index 0a041d98e..cddb22bf7 100644 --- a/src/data/schema/conversation.ts +++ b/src/data/schema/conversation.ts @@ -60,6 +60,7 @@ export const types = ` type ConversationMessage { _id: String! content: String + status: String attachments: [Attachment] mentionedUserIds: [String] conversationId: String From c16f618d84c86621388f1a1650b94b7456dde183 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 26 Jun 2020 16:49:03 -0300 Subject: [PATCH 27/71] update message status --- src/commands/createGooglePubsubTopics.ts | 1 + src/data/modules/integrations/receiveMessage.ts | 8 ++------ src/data/resolvers/subscriptions/conversations.ts | 13 +++++++++++++ src/data/schema/index.ts | 1 + 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/commands/createGooglePubsubTopics.ts b/src/commands/createGooglePubsubTopics.ts index 8d9d53010..e46d1b81e 100644 --- a/src/commands/createGooglePubsubTopics.ts +++ b/src/commands/createGooglePubsubTopics.ts @@ -21,6 +21,7 @@ const topics = [ 'conversationChanged', 'conversationClientMessageInserted', 'conversationMessageInserted', + 'conversationMessageUpdated', 'customerConnectionChanged', 'widgetNotification', ]; diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index bd3644669..0b6325224 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -146,12 +146,8 @@ export const receiveRpcMessage = async msg => { await Conversations.updateConversation(message.conversationId, conversationDoc); - graphqlPubsub.publish('conversationClientMessageInserted', { - conversationClientMessageInserted: message, - }); - - graphqlPubsub.publish('conversationMessageInserted', { - conversationMessageInserted: message, + graphqlPubsub.publish('conversationMessageUpdated', { + conversationMessageUpdated: message, }); return sendSuccess({ _id: doc.id }); diff --git a/src/data/resolvers/subscriptions/conversations.ts b/src/data/resolvers/subscriptions/conversations.ts index d11fae8ea..3d7370318 100644 --- a/src/data/resolvers/subscriptions/conversations.ts +++ b/src/data/resolvers/subscriptions/conversations.ts @@ -29,6 +29,19 @@ export default { ), }, + /* + * Listen for message update + */ + conversationMessageUpdated: { + subscribe: withFilter( + () => graphqlPubsub.asyncIterator('conversationMessageUpdated'), + // filter by conversationId + (payload, variables) => { + return payload.conversationMessageUpdated.conversationId === variables._id; + }, + ), + }, + /* * Admin is listening for this subscription to show typing notification */ diff --git a/src/data/schema/index.ts b/src/data/schema/index.ts index c42d3abf7..c3739673e 100755 --- a/src/data/schema/index.ts +++ b/src/data/schema/index.ts @@ -277,6 +277,7 @@ export const subscriptions = ` type Subscription { conversationChanged(_id: String!): ConversationChangedResponse conversationMessageInserted(_id: String!): ConversationMessage + conversationMessageUpdated(_id: String!): ConversationMessage conversationClientMessageInserted(userId: String!): ConversationMessage conversationClientTypingStatusChanged(_id: String!): ConversationClientTypingStatusChangedResponse conversationAdminMessageInserted(customerId: String!): ConversationAdminMessageInsertedResponse From d278bb31ed398b21323b8ddf64c3901074d00371 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 26 Jun 2020 21:19:14 -0300 Subject: [PATCH 28/71] update avatar from whatspro --- .vscode/launch.json | 38 +++++++++---------- .../modules/integrations/receiveMessage.ts | 1 + 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 30617f745..d1ffc6a6d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,22 +1,18 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "skipFiles": [ - "/**" - ], - "program": "${workspaceFolder}\\dist\\index.js", - "preLaunchTask": "tsc: build - tsconfig.json", - "envFile": "${workspaceRoot}/.env", - "outFiles": [ - "${workspaceFolder}/dist/**/*.js" - ] - } - ] -} \ No newline at end of file + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Program", + "program": "${workspaceFolder}/dist/index.js", + "request": "launch", + "skipFiles": ["/**"], + "type": "pwa-node", + "preLaunchTask": "tsc: build - tsconfig.json", + "envFile": "${workspaceRoot}/.env", + "outFiles": ["${workspaceFolder}/dist/**/*.js"] + } + ] +} diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index 0b6325224..9706123c4 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -48,6 +48,7 @@ export const receiveRpcMessage = async msg => { } if (customer) { + await Customers.updateCustomer(customer._id, doc); return sendSuccess({ _id: customer._id }); } else { customer = await Customers.createCustomer({ From 0b655688845412e881c1c3dee8ee37eb464c0ed0 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 3 Jul 2020 11:49:26 -0300 Subject: [PATCH 29/71] add redis db option --- engages-email-sender/src/redisClient.ts | 3 +++ src/redisClient.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/engages-email-sender/src/redisClient.ts b/engages-email-sender/src/redisClient.ts index 1d0a84527..82ac7c5c0 100644 --- a/engages-email-sender/src/redisClient.ts +++ b/engages-email-sender/src/redisClient.ts @@ -9,11 +9,13 @@ const { REDIS_PORT = 6379, REDIS_PASSWORD, NODE_ENV, + REDIS_DB, }: { REDIS_HOST?: string; REDIS_PORT?: number; REDIS_PASSWORD?: string; NODE_ENV?: string; + REDIS_DB?: number; } = process.env; let client; @@ -23,6 +25,7 @@ export const initRedis = (callback?: (client) => void) => { host: REDIS_HOST, port: REDIS_PORT, password: REDIS_PASSWORD, + db: REDIS_DB, connect_timeout: 15000, enable_offline_queue: true, retry_unfulfilled_commands: true, diff --git a/src/redisClient.ts b/src/redisClient.ts index c13d45e77..c03c4754f 100644 --- a/src/redisClient.ts +++ b/src/redisClient.ts @@ -9,11 +9,13 @@ const { REDIS_PORT = 6379, REDIS_PASSWORD, NODE_ENV, + REDIS_DB, }: { REDIS_HOST?: string; REDIS_PORT?: number; REDIS_PASSWORD?: string; NODE_ENV?: string; + REDIS_DB?: number; } = process.env; /** @@ -24,6 +26,7 @@ export const redisOptions = { host: REDIS_HOST, port: REDIS_PORT, password: REDIS_PASSWORD, + db: REDIS_DB, connect_timeout: 15000, enable_offline_queue: true, retry_unfulfilled_commands: true, From 46667a94cdd3c191eb4020caf362758172cf6769 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 18 Jul 2020 19:11:29 -0300 Subject: [PATCH 30/71] add isOwer to default admin user --- src/db/models/Users.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/db/models/Users.ts b/src/db/models/Users.ts index 3203c4c38..c1c47120b 100644 --- a/src/db/models/Users.ts +++ b/src/db/models/Users.ts @@ -133,7 +133,7 @@ export const loadClass = () => { /** * Create new user */ - public static async createUser({ username, email, password, details, links, groupIds }: IUser) { + public static async createUser({ username, email, password, details, links, groupIds, isOwner }: IUser) { // empty string password validation if (password === '') { throw new Error('Password can not be empty'); @@ -151,6 +151,7 @@ export const loadClass = () => { links, groupIds, isActive: true, + isOwner, // hash password password: await this.generatePassword(password), }); From 4de2983c47e6a679afce732e3c37a7b251e89fd7 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 18 Jul 2020 21:19:42 -0300 Subject: [PATCH 31/71] fix duplicate user declaration --- src/apolloClient.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/apolloClient.ts b/src/apolloClient.ts index d9cf34137..f2df5ca03 100644 --- a/src/apolloClient.ts +++ b/src/apolloClient.ts @@ -64,8 +64,6 @@ const apolloServer = new ApolloServer({ hostname: frontendEnv({ name: 'API_URL', req }), }; - const user = req.user; - if (user) { Users.update( { _id: user._id }, From 8cecea93ce41e0422e5cf9636f616b573e7a03c3 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sun, 19 Jul 2020 11:05:14 -0300 Subject: [PATCH 32/71] fix issue --- src/data/modules/integrations/receiveMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index 8dff9100a..22aa206b0 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -48,7 +48,7 @@ export const receiveRpcMessage = async msg => { } if (customer) { - await Customers.updateCustomer(customer._id, doc); + await Customers.updateCustomer(customer._id, doc, hostname); return sendSuccess({ _id: customer._id }); } else { customer = await Customers.createCustomer({ From 1d89edc085092368b485de4e80e100694e754bc1 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Wed, 22 Jul 2020 17:16:39 -0300 Subject: [PATCH 33/71] release --- Dockerfile | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..68f1c00db --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM node:12.16.3-slim + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y gnupg2 && \ + apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5 && \ + echo "deb http://repo.mongodb.org/apt/debian stretch/mongodb-org/3.6 main" | tee /etc/apt/sources.list.d/mongodb-org-3.6.list + +RUN apt-get update && \ + apt-get install -y \ + rsync python build-essential mongodb-org-shell mongodb-org-tools git nano && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /erxes-api/ + +COPY --chown=node:node . /erxes-api + +RUN chown -R node:node /erxes-api + +USER node + +RUN yarn + +RUN yarn build + +EXPOSE 3300 + +ENTRYPOINT [ "node", "--max_old_space_size=8192", "dist" ] From 83153a9e5831998796d8f78be9cc13ce984694c8 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Wed, 22 Jul 2020 19:11:34 -0300 Subject: [PATCH 34/71] release --- engages-email-sender/.dockerignore | 1 - engages-email-sender/Dockerfile | 27 ++++++++++++++++++++++----- logger/Dockerfile | 25 +++++++++++++++++++++---- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/engages-email-sender/.dockerignore b/engages-email-sender/.dockerignore index 340de91ab..3a7da24c9 100644 --- a/engages-email-sender/.dockerignore +++ b/engages-email-sender/.dockerignore @@ -4,5 +4,4 @@ .git* .prettierrc Dockerfile* -src *.tar.gz diff --git a/engages-email-sender/Dockerfile b/engages-email-sender/Dockerfile index 161a4ea90..8dafd2cf6 100644 --- a/engages-email-sender/Dockerfile +++ b/engages-email-sender/Dockerfile @@ -1,7 +1,24 @@ -FROM node:12.16-slim -WORKDIR /erxes-engages -RUN chown -R node:node /erxes-engages -COPY --chown=node:node . /erxes-engages +FROM node:12.16.3-slim + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y \ + gnupg2 git nano && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /engages-email-sender + +COPY --chown=node:node . /engages-email-sender + +RUN chown -R node:node /engages-email-sender + USER node + +RUN yarn + +RUN yarn build + EXPOSE 3900 -ENTRYPOINT ["node", "--max_old_space_size=8192", "--experimental-worker", "dist"] + +ENTRYPOINT [ "node", "--max_old_space_size=8192", "dist" ] diff --git a/logger/Dockerfile b/logger/Dockerfile index a6279d55c..a84b84202 100644 --- a/logger/Dockerfile +++ b/logger/Dockerfile @@ -1,7 +1,24 @@ -FROM node:12.16-slim -WORKDIR /erxes-logger/ -RUN chown -R node:node /erxes-logger -COPY --chown=node:node . /erxes-logger +FROM node:12.16.3-slim + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y \ + gnupg2 git nano && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /logger + +COPY --chown=node:node . /logger + +RUN chown -R node:node /logger + USER node + +RUN yarn + +RUN yarn build + EXPOSE 3800 + ENTRYPOINT [ "node", "--max_old_space_size=8192", "dist" ] From 4709b8481eeaedd181f18398f7fefa25610a5514 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 23 Jul 2020 08:16:08 -0300 Subject: [PATCH 35/71] fix and improve flow --- src/commands/initFlow.ts | 2 +- src/data/modules/flow/index.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/commands/initFlow.ts b/src/commands/initFlow.ts index bad9a2583..ee6016dad 100644 --- a/src/commands/initFlow.ts +++ b/src/commands/initFlow.ts @@ -23,7 +23,7 @@ connect() 'erxes.action.send.sms', 'erxes.action.define.timeout', 'erxes.action.send.internal.message', - 'erxes.action.execute.autmations.flow', + 'erxes.action.execute.automation.flow', 'erxes.action.execute.action', 'erxes.action.conditional', 'erxes.action.wait.for.interaction', diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index a12efc715..02ba65c04 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -39,7 +39,7 @@ const checkIfIsCondition = (condition: IFlowActionValueCondition, content: strin case 'onboarding_active': let now = moment.utc(); now = now.utcOffset(-180); - let not = [0, 6].includes(now.weekday()) || now.hour() < 9 || now.hour() > 18; + let not = [0].includes(now.weekday()) || now.hour() < 9 || now.hour() > 18; return (condition.variable.value === '0' && not) || (condition.variable.value === '1' && !not); default: @@ -50,6 +50,8 @@ const checkIfIsCondition = (condition: IFlowActionValueCondition, content: strin default: return condition.values.includes(content); } + case '*': + return true; default: return false; } @@ -136,7 +138,7 @@ const handleMessage = async (msg: IMessageDocument) => { // sendNextMessage = true; break; - case 'erxes.action.execute.autmations.flow': + case 'erxes.action.execute.automation.flow': flowAction = await FlowActions.findOne({ flowId: flowAction?.value, order: 0, @@ -254,7 +256,7 @@ const handleMessage = async (msg: IMessageDocument) => { handleTransferToAgent(flowAction, conversation, integration, condition); break; - case 'erxes.action.execute.autmations.flow': + case 'erxes.action.execute.automation.flow': sendNextMessage = true; break; From b22c7f8f79c25726847d8afc890bce8f8661e00b Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 24 Oct 2020 15:01:20 -0300 Subject: [PATCH 36/71] add redis db number --- email-verifier/src/redisClient.ts | 3 +++ src/data/resolvers/mutations/index.ts | 5 +---- src/pubsub.ts | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/email-verifier/src/redisClient.ts b/email-verifier/src/redisClient.ts index 381555b4c..2d613dba4 100644 --- a/email-verifier/src/redisClient.ts +++ b/email-verifier/src/redisClient.ts @@ -9,11 +9,13 @@ const { REDIS_PORT = 6379, REDIS_PASSWORD, NODE_ENV, + REDIS_DB, }: { REDIS_HOST?: string; REDIS_PORT?: number; REDIS_PASSWORD?: string; NODE_ENV?: string; + REDIS_DB?: number; } = process.env; let client; @@ -23,6 +25,7 @@ export const initRedis = (callback?: (client) => void) => { host: REDIS_HOST, port: REDIS_PORT, password: REDIS_PASSWORD, + db: REDIS_DB, connect_timeout: 15000, enable_offline_queue: true, retry_unfulfilled_commands: true, diff --git a/src/data/resolvers/mutations/index.ts b/src/data/resolvers/mutations/index.ts index ba9c26e8f..b1f2afad8 100644 --- a/src/data/resolvers/mutations/index.ts +++ b/src/data/resolvers/mutations/index.ts @@ -74,11 +74,8 @@ export default { ...checklists, ...robot, ...widgets, -<<<<<<< HEAD - ...webhooks, -======= ...flowActionTypes, ...flowActions, ...flows, ->>>>>>> develop + ...webhooks, }; diff --git a/src/pubsub.ts b/src/pubsub.ts index 6f5378e2d..8ccba1b32 100644 --- a/src/pubsub.ts +++ b/src/pubsub.ts @@ -7,13 +7,14 @@ import * as Redis from 'ioredis'; // load environment variables dotenv.config(); -const { REDIS_HOST, REDIS_PORT, REDIS_PASSWORD } = process.env; +const { REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_DB } = process.env; const createPubsubInstance = () => { if (REDIS_HOST) { redisOptions.host = REDIS_HOST; redisOptions.port = REDIS_PORT; redisOptions.password = REDIS_PASSWORD; + redisOptions.db = REDIS_DB; return new RedisPubSub({ connectionListener: error => { From 741a16238d7613633e809e51d2629e89827f288e Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 24 Oct 2020 15:13:29 -0300 Subject: [PATCH 37/71] update env.sample --- .env.sample | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.sample b/.env.sample index 16e2fcbac..28e74382b 100644 --- a/.env.sample +++ b/.env.sample @@ -11,6 +11,7 @@ MONGO_URL=mongodb://localhost/erxes REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= +REDIS_DB= # RabbitMQ RABBITMQ_HOST=amqp://localhost From 1442e9cc53b34d282c71d1860bdbe4e1fe545996 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 24 Oct 2020 15:15:26 -0300 Subject: [PATCH 38/71] fix merge issue --- src/data/resolvers/mutations/integrations.ts | 28 +------------------- src/db/models/definitions/integrations.ts | 5 +--- 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/src/data/resolvers/mutations/integrations.ts b/src/data/resolvers/mutations/integrations.ts index 84945cac3..4f9c489f0 100644 --- a/src/data/resolvers/mutations/integrations.ts +++ b/src/data/resolvers/mutations/integrations.ts @@ -231,7 +231,6 @@ const integrationMutations = { async integrationsRemove(_root, { _id }: { _id: string }, { user, dataSources }: IContext) { const integration = await Integrations.getIntegration(_id); -<<<<<<< HEAD try { if ( [ @@ -252,37 +251,12 @@ const integrationMutations = { 'smooch-line', 'smooch-twilio', 'whatsapp', + 'whatspro', 'telnyx', ].includes(integration.kind) ) { await dataSources.IntegrationsAPI.removeIntegration({ integrationId: _id }); } -======= - if ( - [ - 'facebook-messenger', - 'facebook-post', - 'gmail', - 'callpro', - 'nylas-gmail', - 'nylas-imap', - 'nylas-office365', - 'nylas-outlook', - 'nylas-exchange', - 'nylas-yahoo', - 'chatfuel', - 'twitter-dm', - 'smooch-viber', - 'smooch-telegram', - 'smooch-line', - 'smooch-twilio', - 'whatsapp', - 'whatspro', - ].includes(integration.kind) - ) { - await dataSources.IntegrationsAPI.removeIntegration({ integrationId: _id }); - } ->>>>>>> develop await putDeleteLog({ type: MODULE_NAMES.INTEGRATION, object: integration }, user); diff --git a/src/db/models/definitions/integrations.ts b/src/db/models/definitions/integrations.ts index 828c157a7..59b61ad60 100644 --- a/src/db/models/definitions/integrations.ts +++ b/src/db/models/definitions/integrations.ts @@ -320,11 +320,8 @@ export const integrationSchema = new Schema({ formId: field({ type: String, label: 'Form' }), leadData: field({ type: leadDataSchema, label: 'Lead data' }), isActive: field({ type: Boolean, optional: true, default: true, label: 'Is active' }), -<<<<<<< HEAD - webhookData: field({ type: webhookDataSchema }), -======= flowId: field({ type: String, label: 'Flow' }), ->>>>>>> develop + webhookData: field({ type: webhookDataSchema }), // TODO: remove formData: field({ type: leadDataSchema }), messengerData: field({ type: messengerDataSchema }), From 3fbe5f6323ccf409cb84cd5a99e1d0cf2b5f7138 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 24 Oct 2020 17:43:44 -0300 Subject: [PATCH 39/71] fixes --- src/apolloClient.ts | 2 - src/data/modules/flow/index.ts | 83 ++----------------- src/data/resolvers/mutations/conversations.ts | 9 +- 3 files changed, 17 insertions(+), 77 deletions(-) diff --git a/src/apolloClient.ts b/src/apolloClient.ts index f35054f42..0cb1264fd 100644 --- a/src/apolloClient.ts +++ b/src/apolloClient.ts @@ -62,8 +62,6 @@ const apolloServer = new ApolloServer({ cookies: req.cookies, }; - const user = req.user; - if (user) { Users.update( { _id: user._id }, diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index a12efc715..3d33eaad9 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -1,5 +1,4 @@ import * as moment from 'moment'; -import * as strip from 'strip'; import { ConversationMessages, Conversations, @@ -20,11 +19,14 @@ import { IFlowActionValueCondition, } from '../../../db/models/definitions/flowActions'; import { IConversationDocument } from '../../../db/models/definitions/conversations'; -import { sendMessage } from '../../../messageBroker'; import { KIND_CHOICES } from '../../../db/models/definitions/constants'; -import { IConversationMessageAdd, publishConversationsChanged } from '../../resolvers/mutations/conversations'; +import { + IConversationMessageAdd, + publishConversationsChanged, + sendConversationToIntegrations, + publishMessage, +} from '../../resolvers/mutations/conversations'; import { graphqlPubsub } from '../../../pubsub'; -import Messages from '../../../db/models/ConversationMessages'; import { IIntegrationDocument } from '../../../db/models/definitions/integrations'; import { IDetail, IUserDocument } from '../../../db/models/definitions/users'; @@ -219,7 +221,7 @@ const handleMessage = async (msg: IMessageDocument) => { switch (flowAction.type) { case 'erxes.action.send.message': - await proccessSendMessage(flowAction, conversation); + await processSendMessage(flowAction, conversation); if ( await FlowActions.findOne({ @@ -231,7 +233,7 @@ const handleMessage = async (msg: IMessageDocument) => { break; case 'erxes.action.to.ask': - await proccessSendMessage(flowAction, conversation); + await processSendMessage(flowAction, conversation); break; case 'erxes.action.define.department': @@ -414,7 +416,7 @@ const handleTransferToAgent = async ( } }; -const proccessSendMessage = async (flowAction: IFlowActionDocument, conversation: IConversationDocument) => { +const processSendMessage = async (flowAction: IFlowActionDocument, conversation: IConversationDocument) => { const integration = await Integrations.getIntegration(conversation.integrationId); if (!integration) return; @@ -514,73 +516,6 @@ const handleSendMessage = async ( publishMessage(dbMessage, conversation.customerId); }; -/** - * Send conversation to integrations - */ - -const sendConversationToIntegrations = ( - type: string, - integrationId: string, - conversationId: string, - requestName: string, - doc: IConversationMessageAdd, - dataSources: any, - action?: string, - messageId?: string, -) => { - if (type === 'facebook') { - return sendMessage('erxes-api:integrations-notification', { - action, - type, - payload: JSON.stringify({ - integrationId, - conversationId, - content: strip(doc.content), - attachments: doc.attachments || [], - }), - }); - } - - if (type === 'whatspro') { - doc.content = doc.content.replace(/<\/?(b|strong)>/g, '*'); - doc.content = doc.content.replace(/
/g, '\n'); - doc.content = doc.content.replace(/<\/?i>/g, '_'); - doc.content = doc.content.replace(/<\/?s>/g, '~'); - } - - if (dataSources && dataSources.IntegrationsAPI && requestName) { - return dataSources.IntegrationsAPI[requestName]({ - conversationId, - integrationId, - messageId, - content: strip(doc.content), - attachments: doc.attachments || [], - }); - } -}; - -/** - * Publish admin's message - */ -export const publishMessage = async (message: IMessageDocument, customerId?: string) => { - graphqlPubsub.publish('conversationMessageInserted', { - conversationMessageInserted: message, - }); - - // widget is listening for this subscription to show notification - // customerId available means trying to notify to client - if (customerId) { - const unreadCount = await Messages.widgetsGetUnreadMessagesCount(message.conversationId); - - graphqlPubsub.publish('conversationAdminMessageInserted', { - conversationAdminMessageInserted: { - customerId, - unreadCount, - }, - }); - } -}; - export default { handleMessage, }; diff --git a/src/data/resolvers/mutations/conversations.ts b/src/data/resolvers/mutations/conversations.ts index 8ec893a00..7371da541 100644 --- a/src/data/resolvers/mutations/conversations.ts +++ b/src/data/resolvers/mutations/conversations.ts @@ -39,7 +39,7 @@ interface IReplyFacebookComment { * Send conversation to integrations */ -const sendConversationToIntegrations = ( +export const sendConversationToIntegrations = ( type: string, integrationId: string, conversationId: string, @@ -72,6 +72,13 @@ const sendConversationToIntegrations = ( }); } + if (type === 'whatspro') { + doc.content = doc.content.replace(/<\/?(b|strong)>/g, '*'); + doc.content = doc.content.replace(/
/g, '\n'); + doc.content = doc.content.replace(/<\/?i>/g, '_'); + doc.content = doc.content.replace(/<\/?s>/g, '~'); + } + if (dataSources && dataSources.IntegrationsAPI && requestName) { return dataSources.IntegrationsAPI[requestName]({ conversationId, From cf619a7e31c4dbe6b4b9b7669fd53a9b57bb3c2a Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sun, 22 Nov 2020 09:40:59 -0300 Subject: [PATCH 40/71] remove unused variable --- src/data/modules/integrations/receiveMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index fbed6a542..22c61c45d 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -55,7 +55,7 @@ export const receiveRpcMessage = async msg => { } if (customer) { - await Customers.updateCustomer(customer._id, doc, hostname); + await Customers.updateCustomer(customer._id, doc); return sendSuccess({ _id: customer._id }); } else { customer = await Customers.createCustomer({ From 62dafedacce265049f389ec607b8b9cdf6bf705f Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sun, 22 Nov 2020 10:14:02 -0300 Subject: [PATCH 41/71] fix hostname issue --- src/data/modules/integrations/receiveMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index fbed6a542..22c61c45d 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -55,7 +55,7 @@ export const receiveRpcMessage = async msg => { } if (customer) { - await Customers.updateCustomer(customer._id, doc, hostname); + await Customers.updateCustomer(customer._id, doc); return sendSuccess({ _id: customer._id }); } else { customer = await Customers.createCustomer({ From f689cdfb70d4fdc42b7d9344c49ffae853e27b69 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 17 Dec 2020 21:28:11 -0300 Subject: [PATCH 42/71] implementing on my external message --- src/data/modules/integrations/receiveMessage.ts | 14 ++++++++++++++ src/db/models/definitions/integrations.ts | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index 22c61c45d..60fb4aca9 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -97,6 +97,20 @@ export const receiveRpcMessage = async msg => { } if (action === 'create-conversation-message') { + if (doc.isMe) { + let conversation = await Conversations.findById(doc.conversationId); + + let userId = conversation?.assignedUserId; + + if (!userId) { + const integration = await Integrations.findOne({ _id: doc.integrationId }); + + userId = integration?.defaultSenderId || integration?.createdUserId; + } + + doc.userId = userId; + } + const message = await ConversationMessages.createMessage(doc); const conversationDoc: { diff --git a/src/db/models/definitions/integrations.ts b/src/db/models/definitions/integrations.ts index 59b61ad60..6b6c047fb 100644 --- a/src/db/models/definitions/integrations.ts +++ b/src/db/models/definitions/integrations.ts @@ -117,6 +117,7 @@ export interface IIntegration { export interface IIntegrationDocument extends IIntegration, Document { _id: string; createdUserId: string; + defaultSenderId: string; // TODO remove formData?: ILeadData; leadData?: ILeadDataDocument; @@ -301,7 +302,7 @@ const webhookDataSchema = new Schema( export const integrationSchema = new Schema({ _id: field({ pkey: true }), createdUserId: field({ type: String, label: 'Created by' }), - + defaultSenderId: field({ type: String, label: 'Default sender' }), kind: field({ type: String, enum: KIND_CHOICES.ALL, From 1ea12e8dde245710cc45ce128d9e7e3cf6e7223b Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 18 Dec 2020 20:40:13 -0300 Subject: [PATCH 43/71] implemented external message --- src/data/modules/flow/index.ts | 2 +- src/data/modules/integrations/receiveMessage.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index acdc6821d..a84570dc8 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -60,7 +60,7 @@ const checkIfIsCondition = (condition: IFlowActionValueCondition, content: strin }; const handleMessage = async (msg: IMessageDocument) => { - if (msg.isGroupMsg || !msg.content) return; + if (msg.isGroupMsg || !msg.content || !msg.customerId) return; let conversation = await Conversations.getConversation(msg.conversationId); diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index 60fb4aca9..935a97b18 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -106,6 +106,18 @@ export const receiveRpcMessage = async msg => { const integration = await Integrations.findOne({ _id: doc.integrationId }); userId = integration?.defaultSenderId || integration?.createdUserId; + + if (!userId) { + const user = await Users.findOne({ isOwner: true }); + + userId = user?._id; + } + + if (userId && conversation) { + conversation.assignedUserId = userId; + + await Conversations.updateOne({ _id: conversation._id }, { assignedUserId: conversation.assignedUserId }); + } } doc.userId = userId; From 9e93aed219fd3fc60959bd18ba6a38bc17fa9ed4 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 15 Jan 2021 20:42:56 -0300 Subject: [PATCH 44/71] improved update message --- src/data/modules/integrations/receiveMessage.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index 935a97b18..a0fc3bb86 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -168,7 +168,11 @@ export const receiveRpcMessage = async msg => { if (!message.status || message.status < doc.status) message.status = doc.status; - await ConversationMessages.updateOne({ _id: message._id }, { status: message.status, createdAt: doc.createdAt }); + const update: any = { status: message.status }; + + if (doc.createdAt) update.createdAt = doc.createdAt; + + await ConversationMessages.updateOne({ _id: message._id }, update); const conversationDoc: { updatedAt?: Date; From 8729435a79916eade7b6e4c14b49be57b5b211ae Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 5 Feb 2021 21:05:35 -0300 Subject: [PATCH 45/71] removed assign conversation to user --- src/data/modules/integrations/receiveMessage.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index a0fc3bb86..04e6e3254 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -112,12 +112,6 @@ export const receiveRpcMessage = async msg => { userId = user?._id; } - - if (userId && conversation) { - conversation.assignedUserId = userId; - - await Conversations.updateOne({ _id: conversation._id }, { assignedUserId: conversation.assignedUserId }); - } } doc.userId = userId; From fd5cc90a3ea07853ad47cf888d10ff8a964ccc72 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 11 Feb 2021 09:53:21 -0300 Subject: [PATCH 46/71] improved bot --- package.json | 5 +++-- src/data/modules/flow/index.ts | 35 +++++++++++++++++++++++++++++++--- yarn.lock | 5 +++++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 33b0eb186..e6ee483f6 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,9 @@ "ioredis": "^3.2.2", "json2csv": "^5.0.1", "jsonwebtoken": "^8.1.0", + "libphonenumber-js": "^1.9.10", "meteor-random": "^0.0.3", + "migrate": "^1.6.2", "moment": "^2.18.1", "moment-timezone": "^0.5.31", "mongo-uri": "^0.1.2", @@ -115,8 +117,7 @@ "vm2": "^3.9.2", "xlsx-populate": "^1.20.1", "xlsx-stream-reader": "^1.1.0", - "xss": "^1.0.6", - "migrate": "^1.6.2" + "xss": "^1.0.6" }, "peerOptionalDependencies": { "kerberos": "^1.0.0" diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index a84570dc8..32edd8d13 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -29,12 +29,21 @@ import { import { graphqlPubsub } from '../../../pubsub'; import { IIntegrationDocument } from '../../../db/models/definitions/integrations'; import { IDetail, IUserDocument } from '../../../db/models/definitions/users'; +import { isValidNumber, isValidNumberForRegion } from 'libphonenumber-js'; const actionWithSendNext = ['erxes.action.send.message', 'erxes.action.define.department', 'erxes.action.conditional']; +export function removeDiacritics(string) { + return String(string) + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); +} + const checkIfIsCondition = (condition: IFlowActionValueCondition, content: string = '') => { + content = removeDiacritics(content.toLowerCase()); + switch (condition.operator) { - case '=': + case '=': // Equals switch (condition.type) { case 'erxes.conditional.variable': switch (condition.variable.key) { @@ -50,10 +59,30 @@ const checkIfIsCondition = (condition: IFlowActionValueCondition, content: strin break; default: - return condition.values.includes(content); + return condition.values.map(c => removeDiacritics(c.toLowerCase())).includes(content); } - case '*': + + case '*': // Anything return true; + + case '#': // "Phonenumber" + const number = content.match(/\d/g)?.join('') || ''; + + if (isValidNumber(`+${number}`)) return true; + + if (isValidNumberForRegion(number, 'BR')) return true; + + if (isValidNumberForRegion(number, 'US')) return true; + + return false; + + case '@': // "Email" + return /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/g.test( + content, + ); + + case '%': // "Contains" + return condition.values.map(c => removeDiacritics(c.toLowerCase())).find(c => c.indexOf(content) !== -1); default: return false; } diff --git a/yarn.lock b/yarn.lock index a5c2bacf0..9d5847626 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6376,6 +6376,11 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +libphonenumber-js@^1.9.10: + version "1.9.10" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.10.tgz#40944a824512d74566f3e4a04fd276a11cb3e095" + integrity sha512-XyBYwt1dQCc9emeb78uCqJv9qy9tJQsg6vYDeJt37dwBYiZga8z0rHI5dcrn3aFKz9C5Nn9azaRBC+wmW91FfQ== + lie@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" From e5c40fa531fbd4c8c023818e99358a7f14069a73 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 12 Feb 2021 11:28:42 -0300 Subject: [PATCH 47/71] add image to bot condition check --- src/data/modules/flow/index.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 32edd8d13..d0e78e8b8 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -39,7 +39,7 @@ export function removeDiacritics(string) { .replace(/[\u0300-\u036f]/g, ''); } -const checkIfIsCondition = (condition: IFlowActionValueCondition, content: string = '') => { +const checkIfIsCondition = (condition: IFlowActionValueCondition, content: string = '', attachments: any[] = []) => { content = removeDiacritics(content.toLowerCase()); switch (condition.operator) { @@ -82,14 +82,17 @@ const checkIfIsCondition = (condition: IFlowActionValueCondition, content: strin ); case '%': // "Contains" - return condition.values.map(c => removeDiacritics(c.toLowerCase())).find(c => c.indexOf(content) !== -1); + return Boolean(condition.values.map(c => removeDiacritics(c.toLowerCase())).find(c => c.indexOf(content) !== -1)); + + case 'º': // "Image" + return Boolean(attachments && attachments.length > 0); default: return false; } }; const handleMessage = async (msg: IMessageDocument) => { - if (msg.isGroupMsg || !msg.content || !msg.customerId) return; + if (msg.isGroupMsg || !msg.customerId) return; let conversation = await Conversations.getConversation(msg.conversationId); @@ -182,7 +185,7 @@ const handleMessage = async (msg: IMessageDocument) => { case 'erxes.action.to.ask': { const { conditions }: IFlowActionValue = JSON.parse(flowAction.value || '{}'); - const condition = conditions.find(c => checkIfIsCondition(c, msg.content)); + const condition = conditions.find(c => checkIfIsCondition(c, msg.content, msg.attachments)); if (condition) { switch (condition.action) { @@ -205,7 +208,7 @@ const handleMessage = async (msg: IMessageDocument) => { case 'erxes.action.conditional': { const { conditions }: IFlowActionValue = JSON.parse(flowAction.value || '{}'); - const condition = conditions.find(c => checkIfIsCondition(c, msg.content)); + const condition = conditions.find(c => checkIfIsCondition(c, msg.content, msg.attachments)); if (condition) { switch (condition.action) { From d24dce92330e1c8597a6865572e54715bcd41409 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 18 Mar 2021 08:56:28 -0300 Subject: [PATCH 48/71] implementing executeNext on flow actions --- src/data/modules/flow/index.ts | 2 ++ src/data/schema/flowAction.ts | 1 + src/db/models/definitions/flowActions.ts | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index d0e78e8b8..409c503e3 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -181,6 +181,8 @@ const handleMessage = async (msg: IMessageDocument) => { await Conversations.updateConversation(conversation.id, { currentFlowActionId: flowAction?.id, }); + + sendNextMessage = flowAction?.executeNext ?? false; break; case 'erxes.action.to.ask': { const { conditions }: IFlowActionValue = JSON.parse(flowAction.value || '{}'); diff --git a/src/data/schema/flowAction.ts b/src/data/schema/flowAction.ts index fdfc47796..4f6840cae 100644 --- a/src/data/schema/flowAction.ts +++ b/src/data/schema/flowAction.ts @@ -7,6 +7,7 @@ export const types = ` order: Int! flowId: String! createdAt: Date + executeNext: Boolean! } `; diff --git a/src/db/models/definitions/flowActions.ts b/src/db/models/definitions/flowActions.ts index fdef16ff1..a90c7e4f0 100644 --- a/src/db/models/definitions/flowActions.ts +++ b/src/db/models/definitions/flowActions.ts @@ -9,6 +9,7 @@ export interface IFlowAction { order: number; flowId?: string; actionId?: string; + executeNext?: boolean; } export interface IFlowActionDocument extends IFlowAction, Document { @@ -44,4 +45,5 @@ export const flowActionSchema = new Schema({ value: field({ type: String, optional: true, label: 'Value' }), order: field({ type: Number, label: 'Order' }), createdAt: field({ type: Date, label: 'Created at' }), + executeNext: field({ type: Boolean, optional: true, label: 'Execute next' }), }); From f7f0dce98e665aa7f2b1b31b223431fedfa46a82 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 18 Mar 2021 10:06:09 -0300 Subject: [PATCH 49/71] add timeout on send next message --- src/data/modules/flow/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 409c503e3..8fed07334 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -302,6 +302,7 @@ const handleMessage = async (msg: IMessageDocument) => { } if (sendNextMessage) { + await new Promise(res => setTimeout(res, 1000)); await handleMessage(msg); } }; From 33be9aea0fafa1cf48497912aa341a1a5eec46d7 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 21 May 2021 13:41:20 -0300 Subject: [PATCH 50/71] implemented send file message on bot --- src/data/modules/flow/index.ts | 20 ++++++++++++++++++-- src/db/models/definitions/flowActions.ts | 8 +++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 8fed07334..596e3be4b 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -476,6 +476,20 @@ const processSendMessage = async (flowAction: IFlowActionDocument, conversation: position = Math.round(position * Math.random()); + let message = content[position]; + + let file = ''; + let fileType = ''; + let text = ''; + + if (typeof message === 'string') { + text = message; + } else { + text = message.text; + file = message.file; + fileType = message.fileType; + } + let lastMessage = await ConversationMessages.findOne({ userId: conversation.assignedUserId, conversationId: conversation.id, @@ -485,7 +499,8 @@ const processSendMessage = async (flowAction: IFlowActionDocument, conversation: if ( lastMessage && - content.includes(lastMessage.content || '') && + text && + text.includes(lastMessage.content || '') && moment(lastMessage.createdAt).isAfter(moment().subtract(30, 'minutes')) ) return; @@ -494,7 +509,8 @@ const processSendMessage = async (flowAction: IFlowActionDocument, conversation: conversationId: conversation.id, flowActionId: flowAction.id, internal: false, - content: content[position], + content: text, + attachments: file ? [{ type: fileType, url: file }] : undefined, }; handleSendMessage(integration, conversation, doc, user); diff --git a/src/db/models/definitions/flowActions.ts b/src/db/models/definitions/flowActions.ts index a90c7e4f0..df74e0f63 100644 --- a/src/db/models/definitions/flowActions.ts +++ b/src/db/models/definitions/flowActions.ts @@ -27,8 +27,14 @@ export interface IFlowActionValueCondition { error: string; } +export interface IFlowActionContent { + file: string; + fileType: string; + text: string; +} + export interface IFlowActionValue { - content: string[]; + content: string[] | IFlowActionContent[]; conditions: IFlowActionValueCondition[]; } From 0afcba57e59635e2a9e47850c01254180b8ff10a Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 21 May 2021 13:59:38 -0300 Subject: [PATCH 51/71] add RETURN_CHAT_TO_BOT_AFTER --- src/data/modules/flow/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 596e3be4b..ee71c67ed 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -33,6 +33,8 @@ import { isValidNumber, isValidNumberForRegion } from 'libphonenumber-js'; const actionWithSendNext = ['erxes.action.send.message', 'erxes.action.define.department', 'erxes.action.conditional']; +const { RETURN_CHAT_TO_BOT_AFTER } = process.env; + export function removeDiacritics(string) { return String(string) .normalize('NFD') @@ -106,7 +108,7 @@ const handleMessage = async (msg: IMessageDocument) => { if (!flow) return; - if (conversation.assignedUserId) { + if (conversation.assignedUserId && String(RETURN_CHAT_TO_BOT_AFTER) !== '0') { if (conversation.assignedUserId !== flow.assignedUserId) { let lastMessage = await ConversationMessages.findOne({ userId: conversation.assignedUserId, @@ -115,7 +117,10 @@ const handleMessage = async (msg: IMessageDocument) => { .sort({ createdAt: -1 }) .exec(); - if (lastMessage && moment(lastMessage.createdAt).isAfter(moment().subtract(1, 'day'))) { + if ( + lastMessage && + moment(lastMessage.createdAt).isAfter(moment().subtract(RETURN_CHAT_TO_BOT_AFTER || 24, 'hours')) + ) { return; } From 4fac8f4d342fdaa4065e3340f5e1d04b1b838d17 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 28 May 2021 11:20:39 -0300 Subject: [PATCH 52/71] fix assign to not actived user issue --- src/data/modules/flow/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index ee71c67ed..64a50449f 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -327,6 +327,7 @@ const handleTransferToAgent = async ( { $match: { _id: { $in: channel.memberIds }, + isActive: { $ne: false }, lastSeenAt: { $gte: moment() .subtract(61, 'seconds') From 3f92d04631699f409703f87fe52581fbe2491df1 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 28 May 2021 13:33:07 -0300 Subject: [PATCH 53/71] fix remove assignedUser issue --- src/data/modules/flow/index.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 64a50449f..6b771d4d4 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -108,24 +108,24 @@ const handleMessage = async (msg: IMessageDocument) => { if (!flow) return; - if (conversation.assignedUserId && String(RETURN_CHAT_TO_BOT_AFTER) !== '0') { - if (conversation.assignedUserId !== flow.assignedUserId) { - let lastMessage = await ConversationMessages.findOne({ - userId: conversation.assignedUserId, - conversationId: conversation.id, - }) - .sort({ createdAt: -1 }) - .exec(); - - if ( - lastMessage && - moment(lastMessage.createdAt).isAfter(moment().subtract(RETURN_CHAT_TO_BOT_AFTER || 24, 'hours')) - ) { - return; - } - - conversation.assignedUserId = undefined; + if (conversation.assignedUserId && conversation.assignedUserId !== flow.assignedUserId) { + if (String(RETURN_CHAT_TO_BOT_AFTER) !== '0') return; + + let lastMessage = await ConversationMessages.findOne({ + userId: conversation.assignedUserId, + conversationId: conversation.id, + }) + .sort({ createdAt: -1 }) + .exec(); + + if ( + lastMessage && + moment(lastMessage.createdAt).isAfter(moment().subtract(RETURN_CHAT_TO_BOT_AFTER || 24, 'hours')) + ) { + return; } + + conversation.assignedUserId = undefined; } const user = await Users.findById(flow.assignedUserId); From dd98cc875168446d03bec11828201f39ba99722c Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 28 May 2021 13:59:19 -0300 Subject: [PATCH 54/71] fix reassign issue --- src/data/modules/flow/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 6b771d4d4..2539651e6 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -109,7 +109,7 @@ const handleMessage = async (msg: IMessageDocument) => { if (!flow) return; if (conversation.assignedUserId && conversation.assignedUserId !== flow.assignedUserId) { - if (String(RETURN_CHAT_TO_BOT_AFTER) !== '0') return; + if (String(RETURN_CHAT_TO_BOT_AFTER) === '0') return; let lastMessage = await ConversationMessages.findOne({ userId: conversation.assignedUserId, From 7fb7a03c78926265aa99f29c5134953ab69c1888 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 10 Jun 2021 16:35:34 -0300 Subject: [PATCH 55/71] filter actived users --- src/data/modules/flow/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 2539651e6..da3adf3be 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -395,7 +395,9 @@ const handleTransferToAgent = async ( let users = await Users.aggregate([ { $match: { + _id: { $in: channel.memberIds }, brandIds: { $in: [integration.brandId] }, + isActive: { $ne: false }, }, }, { $sample: { size: 1 } }, @@ -411,6 +413,7 @@ const handleTransferToAgent = async ( { $match: { brandIds: { $in: [integration.brandId] }, + isActive: { $ne: false }, }, }, { $sample: { size: 1 } }, From a8d690400370f812dd508000d3bfa351ee847308 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 9 Dec 2021 07:11:17 -0300 Subject: [PATCH 56/71] update flow action on create if existis --- src/db/models/FlowActions.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/db/models/FlowActions.ts b/src/db/models/FlowActions.ts index 522d4e445..f99e87463 100644 --- a/src/db/models/FlowActions.ts +++ b/src/db/models/FlowActions.ts @@ -26,6 +26,13 @@ export const loadClass = () => { public static async createFlowAction(doc: IFlowAction) { // generate code automatically // if there is no flowAction code defined + + const flowAction = await FlowActions.findOne({ order: doc.order, flowId: doc.flowId }); + + if (flowAction) { + return this.updateFlowAction(flowAction._id, doc); + } + return FlowActions.create({ ...doc, createdAt: new Date(), From 11805dda72a1f909d54f6a40d7c6081b6090db26 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Tue, 19 Apr 2022 11:33:53 -0300 Subject: [PATCH 57/71] implemented transfer to specific agent --- src/data/modules/flow/index.ts | 52 +++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index da3adf3be..6ba2eaafa 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -318,11 +318,11 @@ const handleTransferToAgent = async ( integration: IIntegrationDocument, condition: IFlowActionValueCondition, ) => { - let assignedUserId: string = ''; + let assignedUserId: string = condition.variable?.assignUser || ''; let channel = await Channels.findById(conversation.channelId); - if (channel && channel.memberIds?.length) { + if (!assignedUserId && channel && channel.memberIds?.length) { let users = await Users.aggregate([ { $match: { @@ -367,7 +367,8 @@ const handleTransferToAgent = async ( if (condition.value) { let content = condition.value; - let details: IDetail = assignedUser.details?.toObject() || {}; + let details: IDetail = + (assignedUser.details?.toObject ? assignedUser.details?.toObject() : assignedUser.details) || {}; const keys = Object.keys(details); @@ -395,25 +396,21 @@ const handleTransferToAgent = async ( let users = await Users.aggregate([ { $match: { - _id: { $in: channel.memberIds }, - brandIds: { $in: [integration.brandId] }, - isActive: { $ne: false }, - }, - }, - { $sample: { size: 1 } }, - ]); - - if (users?.length) { - user = users[0]; - } - } - - if (!user) { - let users = await Users.aggregate([ - { - $match: { - brandIds: { $in: [integration.brandId] }, - isActive: { $ne: false }, + $or: [ + { + _id: { $in: channel.memberIds }, + brandIds: { $in: [integration.brandId] }, + isActive: { $ne: false }, + }, + { + _id: { $in: channel.memberIds }, + isActive: { $ne: false }, + }, + { + brandIds: { $in: [integration.brandId] }, + isActive: { $ne: false }, + }, + ], }, }, { $sample: { size: 1 } }, @@ -446,6 +443,15 @@ const handleTransferToAgent = async ( conversationClientMessageInserted: message, }); + let content = condition.error; + let details: IDetail = (user.details?.toObject ? user.details?.toObject() : user.details) || {}; + + const keys = Object.keys(details); + + for (const key of keys) { + content = content.replace(new RegExp(`{{${key}}}`), details[key]); + } + handleSendMessage( integration, conversation, @@ -453,7 +459,7 @@ const handleTransferToAgent = async ( conversationId: conversation.id, flowActionId: flowAction.id, internal: false, - content: condition.error, + content, }, user, ); From 59c296deb904185d8b92e165228b5ed3a0aeb7ce Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Wed, 18 May 2022 11:26:02 -0300 Subject: [PATCH 58/71] add gitignore --- .dockerignore | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..725160722 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +*.md +*.yml +*.yaml +.env* +.git* +.prettierrc +.snyk +Dockerfile* +google_cred.json.sample +scripts +*.tar.gz +automations +elkSyncer +email-verifier +logger +engages-email-sender +dashboard From 8383417ee570a428ed72079a10eadded7f1bf30f Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Thu, 26 May 2022 21:44:08 -0300 Subject: [PATCH 59/71] get last conversation for customer and integration --- .../queries/conversationQueryBuilder.ts | 16 ++++++++++++++++ src/data/schema/conversation.ts | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/data/resolvers/queries/conversationQueryBuilder.ts b/src/data/resolvers/queries/conversationQueryBuilder.ts index e63bfb7e4..a0f1c6c89 100644 --- a/src/data/resolvers/queries/conversationQueryBuilder.ts +++ b/src/data/resolvers/queries/conversationQueryBuilder.ts @@ -15,6 +15,7 @@ interface IExists { export interface IListArgs { limit?: number; + customerId?: string; channelId?: string; status?: string; unassigned?: string; @@ -22,6 +23,7 @@ export interface IListArgs { brandId?: string; tag?: string; integrationType?: string; + integrationId?: string; participating?: string; starred?: string; ids?: string[]; @@ -160,6 +162,13 @@ export default class Builder { }; } + // filter by customer + public async customerFilter(customerId: string): Promise<{ customerId: string }> { + return { + customerId, + }; + } + // filter by brand public async brandFilter(brandId: string): Promise<{ integrationId: IIn }> { const integrations = await Integrations.findIntegrations({ brandId }); @@ -252,6 +261,7 @@ export default class Builder { unassigned: {}, tag: {}, channel: {}, + costumer: {}, integrationType: {}, // find it using channel && brand @@ -266,6 +276,11 @@ export default class Builder { this.queries.channel = await this.channelFilter(this.params.channelId); } + // filter by customer + if (this.params.customerId) { + this.queries.customer = await this.customerFilter(this.params.customerId); + } + // filter by channelId & brandId this.queries.integrations = await this.integrationsFilter(); @@ -316,6 +331,7 @@ export default class Builder { public mainQuery(): any { return { ...this.queries.default, + ...this.queries.customer, ...this.queries.integrations, ...this.queries.integrationType, ...this.queries.unassigned, diff --git a/src/data/schema/conversation.ts b/src/data/schema/conversation.ts index 8f60c01b5..964662ba8 100644 --- a/src/data/schema/conversation.ts +++ b/src/data/schema/conversation.ts @@ -185,12 +185,14 @@ export const types = ` `; const mutationFilterParams = ` + customerId: String channelId: String status: String unassigned: String brandId: String tag: String integrationType: String + integrationId: String participating: String awaitingResponse: String starred: String From 39fab94147a34d37f35e4d997b2745cdbe5bd0ee Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Fri, 27 May 2022 20:36:52 -0300 Subject: [PATCH 60/71] implemented conversationAdd --- src/data/resolvers/mutations/conversations.ts | 22 +++++++++++++++++-- .../queries/conversationQueryBuilder.ts | 2 +- src/data/schema/conversation.ts | 5 +++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/data/resolvers/mutations/conversations.ts b/src/data/resolvers/mutations/conversations.ts index 7371da541..6bde32e4b 100644 --- a/src/data/resolvers/mutations/conversations.ts +++ b/src/data/resolvers/mutations/conversations.ts @@ -9,7 +9,8 @@ import { NOTIFICATION_TYPES, } from '../../../db/models/definitions/constants'; import { IMessageDocument } from '../../../db/models/definitions/conversationMessages'; -import { IConversationDocument } from '../../../db/models/definitions/conversations'; +import { IConversation, IConversationDocument } from '../../../db/models/definitions/conversations'; +import { ICustomerDocument } from '../../../db/models/definitions/customers'; import { IUserDocument } from '../../../db/models/definitions/users'; import { debugExternalApi } from '../../../debuggers'; import messageBroker from '../../../messageBroker'; @@ -48,6 +49,7 @@ export const sendConversationToIntegrations = ( dataSources: any, action?: string, messageId?: string, + customer?: ICustomerDocument | null, ) => { if (type === 'facebook') { const regex = new RegExp(']* src="([^"]*)"', 'g'); @@ -86,6 +88,7 @@ export const sendConversationToIntegrations = ( messageId, content: strip(doc.content), attachments: doc.attachments || [], + customer, }); } }; @@ -213,6 +216,18 @@ const sendNotifications = async ({ }; const conversationMutations = { + /** + * Create new message in conversation + */ + async conversationAdd(_root, doc: IConversation, { user }: IContext) { + const assignedUserId = user ? user._id : undefined; + + doc.assignedUserId = assignedUserId; + + const conversation = await Conversations.createConversation(doc); + + return { _id: conversation._id }; + }, /** * Create new message in conversation */ @@ -325,7 +340,7 @@ const conversationMutations = { requestName = 'replyWhatsPro'; } - await sendConversationToIntegrations( + let r = await sendConversationToIntegrations( type, integrationId, conversationId, @@ -334,8 +349,11 @@ const conversationMutations = { dataSources, action, message._id, + customer, ); + console.log(r); + const dbMessage = await ConversationMessages.getMessage(message._id); await utils.sendToWebhook('create', 'userMessages', dbMessage); diff --git a/src/data/resolvers/queries/conversationQueryBuilder.ts b/src/data/resolvers/queries/conversationQueryBuilder.ts index a0f1c6c89..34321c666 100644 --- a/src/data/resolvers/queries/conversationQueryBuilder.ts +++ b/src/data/resolvers/queries/conversationQueryBuilder.ts @@ -87,7 +87,7 @@ export default class Builder { $or: [ { userId: { $exists: true }, - messageCount: { $gt: 1 }, + messageCount: { $gte: 1 }, ...assignedUserQuery, }, { diff --git a/src/data/schema/conversation.ts b/src/data/schema/conversation.ts index 964662ba8..7060bd053 100644 --- a/src/data/schema/conversation.ts +++ b/src/data/schema/conversation.ts @@ -239,6 +239,11 @@ export const queries = ` `; export const mutations = ` + conversationAdd( + customerId: String, + integrationId: String, + content: String + ): Conversation conversationMessageAdd( conversationId: String, content: String, From 5b07569c57f4fc9b6e00f5d5c2b066184c7fd99a Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sun, 29 May 2022 09:40:40 -0300 Subject: [PATCH 61/71] add flow action erxes.action.define.owner --- src/commands/initFlow.ts | 1 + src/data/modules/flow/index.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/commands/initFlow.ts b/src/commands/initFlow.ts index ee6016dad..85c154c77 100644 --- a/src/commands/initFlow.ts +++ b/src/commands/initFlow.ts @@ -37,6 +37,7 @@ connect() 'erxes.action.send.file', 'erxes.action.execute.javascript', 'erxes.action.define.virtual.agent', + 'erxes.action.define.owner', 'erxes.action.pause', ].map(type => new FlowActionTypes({ type, createdAt: new Date() })); diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 6ba2eaafa..0bc0a8cc8 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -167,6 +167,7 @@ const handleMessage = async (msg: IMessageDocument) => { switch (flowAction.type) { case 'erxes.action.root': case 'erxes.action.define.department': + case 'erxes.action.define.owner': case 'erxes.action.send.message': flowAction = await FlowActions.findOne({ flowId: flowAction.flowId, @@ -282,6 +283,21 @@ const handleMessage = async (msg: IMessageDocument) => { channelId: flowAction.value, }); + if ( + await FlowActions.findOne({ + flowId: flowAction.flowId, + order: flowAction.order + 1, + }) + ) + sendNextMessage = true; + + break; + + case 'erxes.action.define.owner': + Customers.updateCustomer(conversation.customerId as string, { + ownerId: flowAction.value, + }); + if ( await FlowActions.findOne({ flowId: flowAction.flowId, From be44a6af421a05eeb89e7d37b4789171319bf73d Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sun, 29 May 2022 21:38:15 -0300 Subject: [PATCH 62/71] USE_CUSTOMER_RESTRICTIONS --- package.json | 1 + src/commands/loadEsData.ts | 57 +++++++++++++++++++++++++ src/data/modules/coc/customers.ts | 30 +++++++++++++ src/data/resolvers/queries/customers.ts | 12 +++--- 4 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 src/commands/loadEsData.ts diff --git a/package.json b/package.json index e6ee483f6..317f25f4a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "test": "node --expose-gc --max_old_space_size=4000 ./node_modules/.bin/jest --forceExit --silent --logHeapUsage", "loadInitialData": "ts-node ./src/commands/loadInitialData.ts", "loadPermission": "ts-node ./src/commands/loadPermissionData.ts", + "loadEsData": "ts-node --transpile-only ./src/commands/loadEsData.ts", "loadGrowthHackData": "ts-node ./src/commands/loadGrowthHackData.ts", "loadTestData": "ts-node ./src/commands/loadTestData.ts", "initProject": "ts-node ./src/commands/initProject.ts", diff --git a/src/commands/loadEsData.ts b/src/commands/loadEsData.ts new file mode 100644 index 000000000..9adf0d7a4 --- /dev/null +++ b/src/commands/loadEsData.ts @@ -0,0 +1,57 @@ +import { getEnv } from '../data/utils'; +import { connect, disconnect } from '../db/connection'; +import { Companies, Customers } from '../db/models'; +import { fetchElk } from '../elasticsearch'; + +const sendElkRequest = (data, index: string) => { + const body = { ...data }; + + delete body._id; + + return fetchElk('create', index, body, data._id); +}; + +connect() + .then(async () => { + console.log('init'); + + if (!(process.env.MONGO_URL || '').includes('replicaSet')) { + console.log('url'); + + return; + } + + const ELK_SYNCER = getEnv({ name: 'ELK_SYNCER', defaultValue: 'true' }); + + if (ELK_SYNCER === 'true') { + console.log('true'); + + return; + } + + const customers = await Customers.find({}); + + for (const d of customers) { + console.log(d); + + sendElkRequest(d.toJSON(), 'customers'); + } + + const companies = await Companies.find({}); + + for (const d of companies) { + console.log(d); + + sendElkRequest(d.toJSON(), 'companies'); + } + + console.log('done'); + }) + + .then(() => { + return disconnect(); + }) + + .then(() => { + process.exit(); + }); diff --git a/src/data/modules/coc/customers.ts b/src/data/modules/coc/customers.ts index 484d9b054..c10b7e315 100644 --- a/src/data/modules/coc/customers.ts +++ b/src/data/modules/coc/customers.ts @@ -4,10 +4,18 @@ import { Customers, FormSubmissions, Integrations } from '../../../db/models'; import { IConformityQueryParams } from '../../resolvers/queries/types'; import { CommonBuilder } from './utils'; +const { USE_CUSTOMER_RESTRICTIONS } = process.env; + interface ISortParams { [index: string]: number; } +interface IUserArgs { + _id: string; + isOwner?: boolean; + starredConversationIds?: string[]; +} + export const sortBuilder = (params: IListArgs): ISortParams => { const sortField = params.sortField; const sortDirection = params.sortDirection || 0; @@ -40,12 +48,23 @@ export interface IListArgs extends IConformityQueryParams { } export class Builder extends CommonBuilder { + public user: IUserArgs; + constructor(params: IListArgs, context) { super('customers', params, context); + this.user = context.user; this.addStateFilter(); } + public ownerFilter(ownerId) { + this.positiveList.push({ + term: { + ownerId, + }, + }); + } + public addStateFilter() { if (this.params.type) { this.positiveList.push({ @@ -135,8 +154,15 @@ export class Builder extends CommonBuilder { public async findAllMongo(limit: number) { const activeIntegrations = await Integrations.findIntegrations({}, { _id: 1 }); + const ownerSelector: any = {}; + + if (this.user && USE_CUSTOMER_RESTRICTIONS === 'true' && !this.user.isOwner) { + ownerSelector.ownerId = { $in: [this.user._id, null, undefined, ''] }; + } + const selector = { ...this.context.commonQuerySelector, + ...ownerSelector, status: { $ne: 'deleted' }, state: this.params.type || 'customer', $or: [ @@ -165,6 +191,10 @@ export class Builder extends CommonBuilder { public async buildAllQueries(): Promise { await super.buildAllQueries(); + // if (this.user && USE_CUSTOMER_RESTRICTIONS === 'true' && !this.user.isOwner) { + // this.ownerFilter(this.user._id); + // } + // filter by brand if (this.params.brand) { await this.brandFilter(this.params.brand); diff --git a/src/data/resolvers/queries/customers.ts b/src/data/resolvers/queries/customers.ts index e61079126..e3317cc6c 100644 --- a/src/data/resolvers/queries/customers.ts +++ b/src/data/resolvers/queries/customers.ts @@ -42,8 +42,8 @@ const customerQueries = { /** * Customers list */ - async customers(_root, params: IListArgs, { commonQuerySelector, commonQuerySelectorElk }: IContext) { - const qb = new BuildQuery(params, { commonQuerySelector, commonQuerySelectorElk }); + async customers(_root, params: IListArgs, { commonQuerySelector, commonQuerySelectorElk, user }: IContext) { + const qb = new BuildQuery(params, { commonQuerySelector, commonQuerySelectorElk, user }); await qb.buildAllQueries(); @@ -55,8 +55,8 @@ const customerQueries = { /** * Customers for only main list */ - async customersMain(_root, params: IListArgs, { commonQuerySelector, commonQuerySelectorElk }: IContext) { - const qb = new BuildQuery(params, { commonQuerySelector, commonQuerySelectorElk }); + async customersMain(_root, params: IListArgs, { commonQuerySelector, commonQuerySelectorElk, user }: IContext) { + const qb = new BuildQuery(params, { commonQuerySelector, commonQuerySelectorElk, user }); await qb.buildAllQueries(); @@ -68,7 +68,7 @@ const customerQueries = { /** * Group customer counts by brands, segments, integrations, tags */ - async customerCounts(_root, params: ICountParams, { commonQuerySelector, commonQuerySelectorElk }: IContext) { + async customerCounts(_root, params: ICountParams, { commonQuerySelector, commonQuerySelectorElk, user }: IContext) { const { only, type } = params; const counts = { @@ -80,7 +80,7 @@ const customerQueries = { byLeadStatus: {}, }; - const qb = new BuildQuery(params, { commonQuerySelector, commonQuerySelectorElk }); + const qb = new BuildQuery(params, { commonQuerySelector, commonQuerySelectorElk, user }); switch (only) { case 'bySegment': From 7e9196d1cdcb9208c48f369e2ee9153820fdd5a4 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 4 Jun 2022 15:33:29 -0300 Subject: [PATCH 63/71] save button, list and response message --- .../definitions/conversationMessages.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/db/models/definitions/conversationMessages.ts b/src/db/models/definitions/conversationMessages.ts index 4cfe33442..544e3f9de 100644 --- a/src/db/models/definitions/conversationMessages.ts +++ b/src/db/models/definitions/conversationMessages.ts @@ -29,6 +29,9 @@ export interface IMessage { content?: string; createdAt?: Date; attachments?: any; + buttons?: any; + list?: any; + response?: any; mentionedUserIds?: string[]; conversationId: string; internal?: boolean; @@ -62,6 +65,42 @@ const attachmentSchema = new Schema( { _id: false }, ); +const buttonSchema = new Schema( + { + id: field({ type: String, optional: true }), + text: field({ type: String }), + type: field({ type: Number }), + value: field({ type: String, optional: true }), + }, + { _id: false }, +); + +const listItemSchema = new Schema( + { + id: field({ type: String, optional: true }), + text: field({ type: String }), + }, + { _id: false }, +); + +const listSchema = new Schema( + { + title: field({ type: String, optional: true }), + buttonText: field({ type: String, optional: true }), + type: field({ type: Number }), + items: field({ type: [listItemSchema] }), + }, + { _id: false }, +); + +const responseSchema = new Schema( + { + id: field({ type: String }), + text: field({ type: String }), + }, + { _id: false }, +); + const engageDataRuleSchema = new Schema( { kind: field({ type: String }), @@ -90,6 +129,9 @@ export const messageSchema = new Schema({ _id: field({ pkey: true }), content: field({ type: String, optional: true }), attachments: [attachmentSchema], + buttons: [buttonSchema], + list: listSchema, + response: responseSchema, mentionedUserIds: field({ type: [String] }), conversationId: field({ type: String, index: true }), internal: field({ type: Boolean, index: true }), From fab554647e987f8fa1aef7547d86ffcfecb27f29 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 4 Jun 2022 17:40:29 -0300 Subject: [PATCH 64/71] add support to send list and buttons on flow --- src/data/modules/flow/index.ts | 6 ++++++ src/data/resolvers/mutations/conversations.ts | 4 ++++ src/db/models/definitions/flowActions.ts | 2 ++ 3 files changed, 12 insertions(+) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 0bc0a8cc8..a35217213 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -512,6 +512,8 @@ const processSendMessage = async (flowAction: IFlowActionDocument, conversation: let file = ''; let fileType = ''; let text = ''; + let buttons = null; + let list = null; if (typeof message === 'string') { text = message; @@ -519,6 +521,8 @@ const processSendMessage = async (flowAction: IFlowActionDocument, conversation: text = message.text; file = message.file; fileType = message.fileType; + buttons = message.buttons; + list = message.list; } let lastMessage = await ConversationMessages.findOne({ @@ -541,6 +545,8 @@ const processSendMessage = async (flowAction: IFlowActionDocument, conversation: flowActionId: flowAction.id, internal: false, content: text, + buttons, + list, attachments: file ? [{ type: fileType, url: file }] : undefined, }; diff --git a/src/data/resolvers/mutations/conversations.ts b/src/data/resolvers/mutations/conversations.ts index ad9c559d7..88c883a85 100644 --- a/src/data/resolvers/mutations/conversations.ts +++ b/src/data/resolvers/mutations/conversations.ts @@ -27,6 +27,8 @@ export interface IConversationMessageAdd { mentionedUserIds?: string[]; internal?: boolean; attachments?: any; + buttons?: any; + list?: any; flowActionId?: string; } @@ -88,6 +90,8 @@ export const sendConversationToIntegrations = ( messageId, content: strip(doc.content), attachments: doc.attachments || [], + buttons: doc.buttons, + list: doc.list, customer, }); } diff --git a/src/db/models/definitions/flowActions.ts b/src/db/models/definitions/flowActions.ts index df74e0f63..2afcb1075 100644 --- a/src/db/models/definitions/flowActions.ts +++ b/src/db/models/definitions/flowActions.ts @@ -31,6 +31,8 @@ export interface IFlowActionContent { file: string; fileType: string; text: string; + buttons: any; + list: any; } export interface IFlowActionValue { From 83508ca0185c6b19dbfc4f625e5a589093377458 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 4 Jun 2022 18:04:28 -0300 Subject: [PATCH 65/71] handle flow condition based on response --- src/data/modules/flow/index.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index a35217213..dabec4259 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -41,7 +41,12 @@ export function removeDiacritics(string) { .replace(/[\u0300-\u036f]/g, ''); } -const checkIfIsCondition = (condition: IFlowActionValueCondition, content: string = '', attachments: any[] = []) => { +const checkIfIsCondition = ( + condition: IFlowActionValueCondition, + content: string = '', + response: any = null, + attachments: any[] = [], +) => { content = removeDiacritics(content.toLowerCase()); switch (condition.operator) { @@ -61,7 +66,10 @@ const checkIfIsCondition = (condition: IFlowActionValueCondition, content: strin break; default: - return condition.values.map(c => removeDiacritics(c.toLowerCase())).includes(content); + return ( + (response && condition.values.includes(response.id)) || + condition.values.map(c => removeDiacritics(c.toLowerCase())).includes(content) + ); } case '*': // Anything @@ -193,7 +201,7 @@ const handleMessage = async (msg: IMessageDocument) => { case 'erxes.action.to.ask': { const { conditions }: IFlowActionValue = JSON.parse(flowAction.value || '{}'); - const condition = conditions.find(c => checkIfIsCondition(c, msg.content, msg.attachments)); + const condition = conditions.find(c => checkIfIsCondition(c, msg.content, msg.response, msg.attachments)); if (condition) { switch (condition.action) { @@ -216,7 +224,7 @@ const handleMessage = async (msg: IMessageDocument) => { case 'erxes.action.conditional': { const { conditions }: IFlowActionValue = JSON.parse(flowAction.value || '{}'); - const condition = conditions.find(c => checkIfIsCondition(c, msg.content, msg.attachments)); + const condition = conditions.find(c => checkIfIsCondition(c, msg.content, msg.response, msg.attachments)); if (condition) { switch (condition.action) { From 1ba33bb6efeb42a3ae2c776b5f527a360a41c0a2 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 4 Jun 2022 18:26:49 -0300 Subject: [PATCH 66/71] fix status type --- src/db/models/definitions/conversationMessages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/models/definitions/conversationMessages.ts b/src/db/models/definitions/conversationMessages.ts index 544e3f9de..fe37497fa 100644 --- a/src/db/models/definitions/conversationMessages.ts +++ b/src/db/models/definitions/conversationMessages.ts @@ -69,7 +69,7 @@ const buttonSchema = new Schema( { id: field({ type: String, optional: true }), text: field({ type: String }), - type: field({ type: Number }), + type: field({ type: String }), value: field({ type: String, optional: true }), }, { _id: false }, From 158b143d954db3bf6ed6e8cd50fccb2bfd249c71 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 4 Jun 2022 20:32:14 -0300 Subject: [PATCH 67/71] add buttons and list to flow condition --- src/data/modules/flow/index.ts | 4 ++++ src/db/models/definitions/flowActions.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index dabec4259..58e3ca928 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -408,6 +408,8 @@ const handleTransferToAgent = async ( flowActionId: flowAction.id, internal: false, content, + buttons: condition.buttons, + list: condition.list, }, assignedUser, ); @@ -484,6 +486,8 @@ const handleTransferToAgent = async ( flowActionId: flowAction.id, internal: false, content, + buttons: condition.buttons, + list: condition.list, }, user, ); diff --git a/src/db/models/definitions/flowActions.ts b/src/db/models/definitions/flowActions.ts index 2afcb1075..87c47392d 100644 --- a/src/db/models/definitions/flowActions.ts +++ b/src/db/models/definitions/flowActions.ts @@ -25,6 +25,8 @@ export interface IFlowActionValueCondition { value: string; variable: any; error: string; + buttons: any; + list: any; } export interface IFlowActionContent { From 7ef49b3dec7703a19a96648c7ac1aa80b10722fc Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Mon, 6 Jun 2022 17:38:31 -0300 Subject: [PATCH 68/71] add dist and no_modules to dockerignore --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index 725160722..dde7c15d0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,3 +15,5 @@ email-verifier logger engages-email-sender dashboard +dist +node_modules From 6970fde6f8c28dde06a1d238068eab9db535b0d1 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Tue, 16 Aug 2022 08:54:34 -0300 Subject: [PATCH 69/71] fix: Cannot read property 'concat' of undefined --- src/cronJobs/conversations.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cronJobs/conversations.ts b/src/cronJobs/conversations.ts index 887523ecb..88c3d0cc9 100644 --- a/src/cronJobs/conversations.ts +++ b/src/cronJobs/conversations.ts @@ -64,7 +64,9 @@ export const sendMessageEmail = async () => { if (message.attachments.length !== 0) { for (const attachment of message.attachments) { - answer.content = answer.content.concat(`

${attachment.name}

`); + answer.content = (answer.content || '').concat( + `

${attachment.name}

`, + ); } } @@ -89,7 +91,7 @@ export const sendMessageEmail = async () => { if (question.attachments.length !== 0) { for (const attachment of question.attachments) { - questionData.content = questionData.content.concat( + questionData.content = (questionData.content || '').concat( `

${attachment.name}

`, ); } From 46a46c6a7676a23189fc7bc23777edbe607fc418 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Mon, 29 Aug 2022 21:33:34 -0300 Subject: [PATCH 70/71] add curl to docker image --- Dockerfile | 2 +- engages-email-sender/.dockerignore | 1 + engages-email-sender/Dockerfile | 4 ++++ logger/Dockerfile | 4 ++++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 68f1c00db..44bfd9c83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN apt-get update && \ RUN apt-get update && \ apt-get install -y \ - rsync python build-essential mongodb-org-shell mongodb-org-tools git nano && \ + rsync python build-essential mongodb-org-shell mongodb-org-tools git nano curl && \ rm -rf /var/lib/apt/lists/* WORKDIR /erxes-api/ diff --git a/engages-email-sender/.dockerignore b/engages-email-sender/.dockerignore index 3a7da24c9..a13bfec5e 100644 --- a/engages-email-sender/.dockerignore +++ b/engages-email-sender/.dockerignore @@ -2,6 +2,7 @@ *.yml .dockerignore .git* +.env* .prettierrc Dockerfile* *.tar.gz diff --git a/engages-email-sender/Dockerfile b/engages-email-sender/Dockerfile index f07623dbd..ef5b6d6b5 100644 --- a/engages-email-sender/Dockerfile +++ b/engages-email-sender/Dockerfile @@ -1,5 +1,9 @@ FROM node:12.18-slim +RUN apt-get update && \ + apt-get install -y \ + nano curl + WORKDIR /erxes-engages RUN chown -R node:node /erxes-engages COPY --chown=node:node . /erxes-engages diff --git a/logger/Dockerfile b/logger/Dockerfile index 646ac3d7a..a58b218f8 100644 --- a/logger/Dockerfile +++ b/logger/Dockerfile @@ -1,5 +1,9 @@ FROM node:12.18-slim +RUN apt-get update && \ + apt-get install -y \ + nano curl + WORKDIR /erxes-logger/ RUN chown -R node:node /erxes-logger COPY --chown=node:node . /erxes-logger From 87d5177dc52d85d13faf1282fa4dfca9f9b7baf5 Mon Sep 17 00:00:00 2001 From: Jonatan Rinckus Date: Sat, 22 Oct 2022 11:06:17 -0300 Subject: [PATCH 71/71] implemented ibm watson flow connection --- package.json | 1 + src/data/modules/flow/WatsonAssistant.ts | 111 ++++++ src/data/modules/flow/index.ts | 150 +++++++- src/data/schema/flow.ts | 5 + src/db/models/definitions/flows.ts | 10 + yarn.lock | 430 ++++++++++++++++++++++- 6 files changed, 690 insertions(+), 17 deletions(-) create mode 100644 src/data/modules/flow/WatsonAssistant.ts diff --git a/package.json b/package.json index 317f25f4a..1feb5351f 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "graphql-tools": "^4.0.3", "handlebars": "^4.7.3", "helmet": "^3.23.3", + "ibm-watson": "^7.1.1", "ioredis": "^3.2.2", "json2csv": "^5.0.1", "jsonwebtoken": "^8.1.0", diff --git a/src/data/modules/flow/WatsonAssistant.ts b/src/data/modules/flow/WatsonAssistant.ts new file mode 100644 index 000000000..80854da6a --- /dev/null +++ b/src/data/modules/flow/WatsonAssistant.ts @@ -0,0 +1,111 @@ +import AssistantV2 = require('ibm-watson/assistant/v2'); +import { IamAuthenticator } from 'ibm-watson/auth'; +import { Conversations } from '../../../db/models'; +import { IConversationDocument } from '../../../db/models/definitions/conversations'; +import { ICustomerDocument } from '../../../db/models/definitions/customers'; + +interface Config { + apiKey: string; + serviceUrl: string; + watsonAssistantId: string; +} + +class WatsonAssistant { + assistant: AssistantV2; + config: Config; + + constructor(config: Config) { + this.config = config; + this.assistant = new AssistantV2({ + version: '2020-09-24', + authenticator: new IamAuthenticator({ apikey: config.apiKey }), + serviceUrl: config.serviceUrl, + }); + } + + async buildContext(customer: ICustomerDocument): Promise { + const user_defined: AssistantV2.JsonObject = {}; + + console.log(user_defined); + + user_defined.firstName = customer.firstName; + user_defined.lastName = customer.lastName; + + return { + skills: { + 'main skill': { + user_defined, + }, + }, + global: { + system: { + user_id: String(customer._id), + }, + }, + }; + } + + async createSession() { + const response = await this.assistant.createSession({ + assistantId: this.config.watsonAssistantId, + }); + + return response.result.session_id; + } + + async getSession(conversation: IConversationDocument) { + if (!conversation.currentFlowActionId) { + conversation.currentFlowActionId = await this.createSession(); + } + + return conversation; + } + + sendToWatson( + conversation: IConversationDocument, + text: string, + context: AssistantV2.MessageContext, + intents?: AssistantV2.RuntimeIntent[], + ): Promise> { + return new Promise(async (resolve, reject) => { + this.assistant + .message({ + assistantId: this.config.watsonAssistantId, + sessionId: conversation.currentFlowActionId!, + input: { + text, + message_type: 'text', + options: { return_context: true, debug: true, export: true }, + intents, + }, + context, + }) + .then(res => { + resolve(res); + }) + .catch(async err => { + console.log(err); + + if (err.message.toLowerCase().indexOf('session') > -1) { + conversation.currentFlowActionId = ''; + + await Conversations.updateConversation(conversation._id, conversation); + + this.getSession(conversation) + .then(conversation => { + Conversations.updateConversation(conversation._id, conversation).then(() => { + this.sendToWatson(conversation, text, context, intents) + .then(resolve) + .catch(reject); + }); + }) + .catch(reject); + } else { + reject(err); + } + }); + }); + } +} + +export default WatsonAssistant; diff --git a/src/data/modules/flow/index.ts b/src/data/modules/flow/index.ts index 58e3ca928..ad82fcf8d 100644 --- a/src/data/modules/flow/index.ts +++ b/src/data/modules/flow/index.ts @@ -30,6 +30,10 @@ import { graphqlPubsub } from '../../../pubsub'; import { IIntegrationDocument } from '../../../db/models/definitions/integrations'; import { IDetail, IUserDocument } from '../../../db/models/definitions/users'; import { isValidNumber, isValidNumberForRegion } from 'libphonenumber-js'; +import { IFlowDocument } from '../../../db/models/definitions/flows'; + +import WatsonAssistant from './WatsonAssistant'; +import AssistantV2 = require('ibm-watson/assistant/v2'); const actionWithSendNext = ['erxes.action.send.message', 'erxes.action.define.department', 'erxes.action.conditional']; @@ -101,6 +105,124 @@ const checkIfIsCondition = ( } }; +const handleMessageWatson = async ( + integration: IIntegrationDocument, + flow: IFlowDocument, + user: IUserDocument, + conversation: IConversationDocument, + msg: IMessageDocument, +) => { + try { + const assistant = new WatsonAssistant({ + apiKey: flow.watsonApiKey!, + serviceUrl: flow.watsonAssistantUrl!, + watsonAssistantId: flow.watsonAssistantId!, + }); + + const text = removeDiacritics(msg.content); + + const customer = await Customers.findById(msg.customerId); + + if (!customer) return; + + const context = await assistant.buildContext(customer); + + const response = await assistant.sendToWatson(conversation, text, context); + + await handleWatsonResponse(assistant, integration, user, conversation, msg, response); + } catch (error) { + console.log(error); + } +}; + +const handleWatsonResponse = async ( + assistant: WatsonAssistant, + integration: IIntegrationDocument, + user: IUserDocument, + conversation: IConversationDocument, + msg: IMessageDocument, + response: AssistantV2.Response, +) => { + if (!response) return; + + const { user_defined } = response.result.context!.skills!['main skill']; + + if (user_defined.transfer_to_human) { + await handleTransferToAgent(conversation, integration); + return; + } + + for (const output of ((response.result.output.generic || []) as any[]).sort(a => + ['option', 'text'].indexOf(a.response_type), + )) { + const doc: IConversationMessageAdd = { + conversationId: conversation.id, + flowActionId: output.id, //check + internal: false, + content: '', + buttons: undefined, + list: undefined, + attachments: undefined, //file ? [{ type: fileType, url: file }] : undefined, + }; + + switch (output.response_type) { + case 'text': { + doc.content = output.text; + + break; + } + case 'image': + case 'video': + case 'audio': { + doc.content = output.title; + + doc.attachments = [{ type: output.response_type, url: output.source }]; + + break; + } + case 'option': { + doc.content = output.title; + + if (output.options.length <= 3) { + doc.buttons = output.options.map(c => ({ + text: c.label, + id: c.value?.input.text, + type: 'default', + })); + } else { + doc.list = {}; + + doc.list.items = output.options.map(c => ({ + text: c.label, + id: c.value?.input.text, + })); + } + + break; + } + case 'suggestion': { + const intent = response.result.output.intents![0]; + + if (intent?.confidence! > 0.5) { + const text = removeDiacritics(msg.content); + response = await assistant.sendToWatson(conversation, text, response.result.context!, [intent]); + + return handleWatsonResponse(assistant, integration, user, conversation, msg, response); + } else { + // todo handle suggestions + } + break; + } + default: + break; + } + + await new Promise(res => setTimeout(res, 2000)); + + handleSendMessage(integration, conversation, doc, user); + } +}; + const handleMessage = async (msg: IMessageDocument) => { if (msg.isGroupMsg || !msg.customerId) return; @@ -140,6 +262,8 @@ const handleMessage = async (msg: IMessageDocument) => { if (!user) return; + if (flow.type === 'watson') return await handleMessageWatson(integration, flow, user, conversation, msg); + if (!conversation.assignedUserId) { const conversations: IConversationDocument[] = await Conversations.assignUserConversation( [conversation.id], @@ -213,7 +337,7 @@ const handleMessage = async (msg: IMessageDocument) => { break; case 'erxes.action.transfer.to.agent': - await handleTransferToAgent(flowAction, conversation, integration, condition); + await handleTransferToAgent(conversation, integration, condition, flowAction); return; default: break; @@ -318,7 +442,7 @@ const handleMessage = async (msg: IMessageDocument) => { case 'erxes.action.transfer.to.agent': const condition = JSON.parse(flowAction.value || '{}'); - handleTransferToAgent(flowAction, conversation, integration, condition); + handleTransferToAgent(conversation, integration, condition, flowAction); break; case 'erxes.action.execute.automation.flow': @@ -337,12 +461,12 @@ const handleMessage = async (msg: IMessageDocument) => { }; const handleTransferToAgent = async ( - flowAction: IFlowActionDocument, conversation: IConversationDocument, integration: IIntegrationDocument, - condition: IFlowActionValueCondition, + condition?: IFlowActionValueCondition, + flowAction?: IFlowActionDocument, ) => { - let assignedUserId: string = condition.variable?.assignUser || ''; + let assignedUserId: string = condition?.variable?.assignUser || ''; let channel = await Channels.findById(conversation.channelId); @@ -389,7 +513,7 @@ const handleTransferToAgent = async ( conversationClientMessageInserted: message, }); - if (condition.value) { + if (condition?.value) { let content = condition.value; let details: IDetail = (assignedUser.details?.toObject ? assignedUser.details?.toObject() : assignedUser.details) || {}; @@ -405,7 +529,7 @@ const handleTransferToAgent = async ( conversation, { conversationId: conversation.id, - flowActionId: flowAction.id, + flowActionId: flowAction?.id, internal: false, content, buttons: condition.buttons, @@ -415,7 +539,7 @@ const handleTransferToAgent = async ( ); } } - } else if (condition.error) { + } else if (condition!.error) { let user: IUserDocument | null = null; if (channel && channel.memberIds?.length) { @@ -469,7 +593,7 @@ const handleTransferToAgent = async ( conversationClientMessageInserted: message, }); - let content = condition.error; + let content = condition!.error; let details: IDetail = (user.details?.toObject ? user.details?.toObject() : user.details) || {}; const keys = Object.keys(details); @@ -483,11 +607,11 @@ const handleTransferToAgent = async ( conversation, { conversationId: conversation.id, - flowActionId: flowAction.id, + flowActionId: flowAction?.id, internal: false, content, - buttons: condition.buttons, - list: condition.list, + buttons: condition?.buttons, + list: condition?.list, }, user, ); @@ -554,7 +678,7 @@ const processSendMessage = async (flowAction: IFlowActionDocument, conversation: const doc: IConversationMessageAdd = { conversationId: conversation.id, - flowActionId: flowAction.id, + flowActionId: flowAction?.id, internal: false, content: text, buttons, diff --git a/src/data/schema/flow.ts b/src/data/schema/flow.ts index 10162d03e..5d6c9b05e 100644 --- a/src/data/schema/flow.ts +++ b/src/data/schema/flow.ts @@ -7,6 +7,11 @@ export const types = ` integrations: [Integration] assignedUserId: String createdAt: Date + type: String + watsonApiKey: String + watsonAssistantId: String + watsonSkillId: String + watsonAssistantUrl: String } `; diff --git a/src/db/models/definitions/flows.ts b/src/db/models/definitions/flows.ts index 2522d801b..d120d9eaa 100644 --- a/src/db/models/definitions/flows.ts +++ b/src/db/models/definitions/flows.ts @@ -5,6 +5,11 @@ export interface IFlow { name?: string; description?: string; assignedUserId?: string; + type?: string; + watsonApiKey?: string; + watsonAssistantId?: string; + watsonSkillId?: string; + watsonAssistantUrl?: string; } export interface IFlowDocument extends IFlow, Document { @@ -21,4 +26,9 @@ export const flowSchema = new Schema({ userId: field({ type: String, label: 'Created by' }), assignedUserId: field({ type: String }), createdAt: field({ type: Date, label: 'Created at' }), + type: field({ type: String, optional: true }), + watsonApiKey: field({ type: String, optional: true }), + watsonAssistantId: field({ type: String, optional: true }), + watsonSkillId: field({ type: String, optional: true }), + watsonAssistantUrl: field({ type: String, optional: true }), }); diff --git a/yarn.lock b/yarn.lock index 9d5847626..82670867f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -540,6 +540,17 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -755,6 +766,11 @@ dependencies: "@types/node" "*" +"@types/async@^3.2.5": + version "3.2.15" + resolved "https://registry.yarnpkg.com/@types/async/-/async-3.2.15.tgz#26d4768fdda0e466f18d6c9918ca28cc89a4e1fe" + integrity sha512-PAmPfzvFA31mRoqZyTVsgJMsvbynR429UTTxhmfsUCrWGh3/fxOrzqBtaTPJsn4UtzTv4Vb0+/O7CARWb69N4g== + "@types/babel__core@^7.1.0": version "7.1.6" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.6.tgz#16ff42a5ae203c9af1c6e190ed1f30f83207b610" @@ -920,6 +936,18 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/extend@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/extend/-/extend-3.0.1.tgz#923dc2d707d944382433e01d6cc0c69030ab2c75" + integrity sha512-R1g/VyKFFI2HLC1QGAeTtCBWCo6n75l41OnsVYNbmKG+kempOESaodf6BeJyUM3Q0rKa/NQcTHbB2+66lNnxLw== + +"@types/file-type@~5.2.1": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@types/file-type/-/file-type-5.2.2.tgz#901cda395f75780c52bbc7a56fd1f5b5bc96f28f" + integrity sha512-GWtM4fyqfb+bec4ocpo51/y4x0b83Je+iA6eV131LT9wL0//G+1UgwbkMg7w61ceOwR+KkZXK00z44jrrNljWg== + dependencies: + "@types/node" "*" + "@types/find-cache-dir@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/find-cache-dir/-/find-cache-dir-2.0.1.tgz#7d44d34f6fcde70989517cb4f66042a9bf83f5e3" @@ -1000,6 +1028,11 @@ "@types/bluebird" "*" "@types/node" "*" +"@types/isstream@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@types/isstream/-/isstream-0.1.0.tgz#f6522f52c0903ad5dc153950a720321928309fa1" + integrity sha512-jo6R5XtVMgu1ej3H4o9NXiUE/4ZxyxmDrGslGiBa4/ugJr+Olw2viio/F2Vlc+zrwC9HJzuApOCCVC2g5jqV0w== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -1020,6 +1053,13 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + "@types/jest@^24.0.21": version "24.9.1" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.9.1.tgz#02baf9573c78f1b9974a5f36778b366aa77bd534" @@ -1123,11 +1163,21 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.17.tgz#7a183163a9e6ff720d86502db23ba4aade5999b8" integrity sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q== +"@types/node@^13.13.39": + version "13.13.52" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.52.tgz#03c13be70b9031baaed79481c0c0cfb0045e53f7" + integrity sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ== + "@types/node@^8.10.59": version "8.10.59" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.59.tgz#9e34261f30183f9777017a13d185dfac6b899e04" integrity sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ== +"@types/node@~10.14.19": + version "10.14.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.22.tgz#34bcdf6b6cb5fc0db33d24816ad9d3ece22feea4" + integrity sha512-9taxKC944BqoTVjE+UT3pQH0nHZlTvITwfsOZqyc+R3sfJuxaTtxWjfn1K2UlxyPcKHf0rnaXcVFrS9F9vf0bw== + "@types/qs@*": version "6.9.1" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.1.tgz#937fab3194766256ee09fcd40b781740758617e7" @@ -1161,6 +1211,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + "@types/strip-bom@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" @@ -1181,6 +1236,11 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.6.tgz#c880579e087d7a0db13777ff8af689f4ffc7b0d5" integrity sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ== +"@types/tough-cookie@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" + integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== + "@types/underscore@^1.8.9": version "1.9.4" resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.9.4.tgz#22d1a3e6b494608e430221ec085fa0b7ccee7f33" @@ -1193,6 +1253,13 @@ dependencies: "@types/node" "*" +"@types/websocket@^1.0.1": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.5.tgz#3fb80ed8e07f88e51961211cd3682a3a4a81569c" + integrity sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ== + dependencies: + "@types/node" "*" + "@types/ws@^6.0.0": version "6.0.4" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1" @@ -1212,6 +1279,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^15.0.0": + version "15.0.14" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06" + integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ== + dependencies: + "@types/yargs-parser" "*" + "@wry/equality@^0.1.2": version "0.1.9" resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.9.tgz#b13e18b7a8053c6858aa6c85b54911fb31e3a909" @@ -1396,6 +1470,13 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" @@ -1762,6 +1843,11 @@ async@^2.6.2: dependencies: lodash "^4.17.14" +async@^3.2.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1802,6 +1888,21 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== +axios-cookiejar-support@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/axios-cookiejar-support/-/axios-cookiejar-support-1.0.1.tgz#7b32af7d932508546c68b1fc5ba8f562884162e1" + integrity sha512-IZJxnAJ99XxiLqNeMOqrPbfR7fRyIfaoSLdPUf4AMQEGkH8URs0ghJK/xtqBsD+KsSr3pKl4DEQjCn834pHMig== + dependencies: + is-redirect "^1.0.0" + pify "^5.0.0" + +axios@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -2324,6 +2425,13 @@ buffers@~0.1.1: resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= +bufferutil@^4.0.1: + version "4.0.7" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad" + integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw== + dependencies: + node-gyp-build "^4.3.0" + builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -2421,6 +2529,11 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + camelize@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" @@ -2483,6 +2596,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + changelog-filename-regex@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/changelog-filename-regex/-/changelog-filename-regex-1.1.2.tgz#19e98e38248cff0c1cf3ae3bf51bfb22c48592d6" @@ -3008,6 +3129,14 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -3306,6 +3435,11 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== +diff-sequences@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" + integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== + diff@^3.1.0, diff@^3.2.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -3355,6 +3489,11 @@ dotenv@^4.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" integrity sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0= +dotenv@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064" + integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w== + double-ended-queue@^2.1.0-0: version "2.1.0-0" resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" @@ -3595,6 +3734,32 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.50: + version "0.10.62" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" + integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + escape-goat@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" @@ -3615,6 +3780,11 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escodegen@^1.9.1: version "1.14.1" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" @@ -3769,6 +3939,18 @@ expect@^24.9.0: jest-message-util "^24.9.0" jest-regex-util "^24.9.0" +expect@^26.1.0: + version "26.6.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" + integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== + dependencies: + "@jest/types" "^26.6.2" + ansi-styles "^4.0.0" + jest-get-type "^26.3.0" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-regex-util "^26.0.0" + express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -3805,6 +3987,13 @@ express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +ext@^1.1.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -3975,6 +4164,11 @@ file-type@^6.1.0: resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== +file-type@^7.7.1: + version "7.7.1" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-7.7.1.tgz#91c2f5edb8ce70688b9b68a90d931bbb6cb21f65" + integrity sha512-bTrKkzzZI6wH+NXhyD3SOXtb2zXTw2SbwI2RxUlRcXVsnN7jNL5hJzVQLYv7FOQhxFkK4XWdAflEaWFpaLLWpQ== + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -4113,6 +4307,11 @@ flexbuffer@0.0.6: resolved "https://registry.yarnpkg.com/flexbuffer/-/flexbuffer-0.0.6.tgz#039fdf23f8823e440c38f3277e6fef1174215b30" integrity sha1-A5/fI/iCPkQMOPMnfm/vEXQhWzA= +follow-redirects@^1.14.8: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -4139,7 +4338,7 @@ form-data@3.0.0, form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^2.5.0: +form-data@^2.3.3, form-data@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== @@ -4526,6 +4725,11 @@ graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +graceful-fs@^4.2.4: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + "graceful-readlink@>= 1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" @@ -4882,6 +5086,51 @@ husky@^0.13.4: is-ci "^1.0.9" normalize-path "^1.0.0" +ibm-cloud-sdk-core@^2.17.15: + version "2.17.15" + resolved "https://registry.yarnpkg.com/ibm-cloud-sdk-core/-/ibm-cloud-sdk-core-2.17.15.tgz#74f2339f9d9faae3b7df5678c881ca2de7830a7f" + integrity sha512-D7olUTiD4um58hWDNYhWL17W0PLxzkCT/XPcHYM3W4yZTizSILvc0e/RsKSTj/KsOocfKJL9jQKv9thxP3zkgQ== + dependencies: + "@types/file-type" "~5.2.1" + "@types/isstream" "^0.1.0" + "@types/node" "~10.14.19" + "@types/tough-cookie" "^4.0.0" + axios "^0.26.1" + axios-cookiejar-support "^1.0.0" + camelcase "^5.3.1" + debug "^4.1.1" + dotenv "^6.2.0" + expect "^26.1.0" + extend "^3.0.2" + file-type "^7.7.1" + form-data "^2.3.3" + isstream "~0.1.2" + jsonwebtoken "^8.5.1" + lodash.isempty "^4.4.0" + mime-types "~2.1.18" + object.omit "~3.0.0" + object.pick "~1.3.0" + retry-axios "^2.6.0" + semver "^6.2.0" + tough-cookie "^4.0.0" + +ibm-watson@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/ibm-watson/-/ibm-watson-7.1.1.tgz#ae4f5723a814de8833595011f4c2946025bbfb2b" + integrity sha512-GE7nLhuwWf75lxHQt1UOace4IYJSBrSZRJzpmYo6qaM8eTQKz25ljr5bLJ4lmZ/tB27c1+g2OR9s4vI6n7eXVw== + dependencies: + "@types/async" "^3.2.5" + "@types/extend" "^3.0.1" + "@types/isstream" "^0.1.0" + "@types/node" "^13.13.39" + "@types/websocket" "^1.0.1" + async "^3.2.0" + camelcase "^6.2.0" + extend "~3.0.2" + ibm-cloud-sdk-core "^2.17.15" + isstream "~0.1.2" + websocket "^1.0.33" + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -5188,7 +5437,7 @@ is-extendable@^0.1.0, is-extendable@^0.1.1: resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= -is-extendable@^1.0.1: +is-extendable@^1.0.0, is-extendable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== @@ -5354,6 +5603,11 @@ is-promise@^2.1.0: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + integrity sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw== + is-regex@^1.0.4, is-regex@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" @@ -5643,6 +5897,16 @@ jest-diff@^24.3.0, jest-diff@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" +jest-diff@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" + integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== + dependencies: + chalk "^4.0.0" + diff-sequences "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + jest-docblock@^24.3.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" @@ -5711,6 +5975,11 @@ jest-get-type@^24.3.0, jest-get-type@^24.9.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== +jest-get-type@^26.3.0: + version "26.3.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" + integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== + jest-haste-map@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" @@ -5796,6 +6065,16 @@ jest-matcher-utils@^24.7.0, jest-matcher-utils@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" +jest-matcher-utils@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" + integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== + dependencies: + chalk "^4.0.0" + jest-diff "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + jest-message-util@^22.4.0, jest-message-util@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-22.4.3.tgz#cf3d38aafe4befddbfc455e57d65d5239e399eb7" @@ -5821,6 +6100,21 @@ jest-message-util@^24.9.0: slash "^2.0.0" stack-utils "^1.0.1" +jest-message-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" + integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/types" "^26.6.2" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" + micromatch "^4.0.2" + pretty-format "^26.6.2" + slash "^3.0.0" + stack-utils "^2.0.2" + jest-mock@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-22.4.3.tgz#f63ba2f07a1511772cdc7979733397df770aabc7" @@ -5848,6 +6142,11 @@ jest-regex-util@^24.3.0, jest-regex-util@^24.9.0: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA== +jest-regex-util@^26.0.0: + version "26.0.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" + integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== + jest-resolve-dependencies@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz#ad055198959c4cfba8a4f066c673a3f0786507ab" @@ -6223,7 +6522,7 @@ jsonwebtoken@8.1.0: ms "^2.0.0" xtend "^4.0.1" -jsonwebtoken@^8.1.0: +jsonwebtoken@^8.1.0, jsonwebtoken@^8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== @@ -6976,6 +7275,11 @@ mime-db@1.43.0, "mime-db@>= 1.43.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + mime-types@2.1.26, mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.26" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" @@ -6983,6 +7287,13 @@ mime-types@2.1.26, mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.19, mi dependencies: mime-db "1.43.0" +mime-types@~2.1.18: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -7251,6 +7562,11 @@ neo-async@^2.6.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -7287,6 +7603,11 @@ node-forge@^0.9.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== +node-gyp-build@^4.3.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" + integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -7469,7 +7790,14 @@ object.omit@^2.0.0: for-own "^0.1.4" is-extendable "^0.1.1" -object.pick@^1.3.0: +object.omit@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-3.0.0.tgz#0e3edc2fce2ba54df5577ff529f6d97bd8a522af" + integrity sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ== + dependencies: + is-extendable "^1.0.0" + +object.pick@^1.3.0, object.pick@~1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= @@ -7871,6 +8199,11 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +pify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" + integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -7968,6 +8301,16 @@ pretty-format@^24.9.0: ansi-styles "^3.2.0" react-is "^16.8.4" +pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + printj@~1.1.0, printj@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" @@ -8042,6 +8385,11 @@ psl@^1.1.28: resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -8145,6 +8493,11 @@ react-is@^16.8.4: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527" integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA== +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -8561,6 +8914,11 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry-axios@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-2.6.0.tgz#d4dc5c8a8e73982e26a705e46a33df99a28723e0" + integrity sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ== + retry-request@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.1.1.tgz#f676d0db0de7a6f122c048626ce7ce12101d2bd8" @@ -9099,6 +9457,13 @@ stack-utils@^1.0.1: resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== +stack-utils@^2.0.2: + version "2.0.5" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" + integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== + dependencies: + escape-string-regexp "^2.0.0" + staged-git-files@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-0.0.4.tgz#d797e1b551ca7a639dec0237dc6eb4bb9be17d35" @@ -9544,6 +9909,16 @@ tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" +tough-cookie@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" + integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" @@ -9761,6 +10136,16 @@ type-is@^1.6.16, type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -9840,6 +10225,11 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -9911,6 +10301,14 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + url-parse@~1.4.3: version "1.4.7" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" @@ -9932,6 +10330,13 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -10057,6 +10462,18 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== +websocket@^1.0.33: + version "1.0.34" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.34.tgz#2bdc2602c08bf2c82253b730655c0ef7dcab3111" + integrity sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ== + dependencies: + bufferutil "^4.0.1" + debug "^2.2.0" + es5-ext "^0.10.50" + typedarray-to-buffer "^3.1.5" + utf-8-validate "^5.0.2" + yaeti "^0.0.6" + whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" @@ -10302,6 +10719,11 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug== + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"