diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..dde7c15d0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +*.md +*.yml +*.yaml +.env* +.git* +.prettierrc +.snyk +Dockerfile* +google_cred.json.sample +scripts +*.tar.gz +automations +elkSyncer +email-verifier +logger +engages-email-sender +dashboard +dist +node_modules 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 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..d1ffc6a6d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +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": [ + { + "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/Dockerfile b/Dockerfile new file mode 100644 index 000000000..44bfd9c83 --- /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 curl && \ + 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" ] diff --git a/elkSyncer/main.py b/elkSyncer/main.py index 5e71260a7..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', @@ -153,7 +159,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) 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/engages-email-sender/.dockerignore b/engages-email-sender/.dockerignore index 340de91ab..a13bfec5e 100644 --- a/engages-email-sender/.dockerignore +++ b/engages-email-sender/.dockerignore @@ -2,7 +2,7 @@ *.yml .dockerignore .git* +.env* .prettierrc Dockerfile* -src *.tar.gz diff --git a/engages-email-sender/Dockerfile b/engages-email-sender/Dockerfile index 8c1a58cf1..ef5b6d6b5 100644 --- a/engages-email-sender/Dockerfile +++ b/engages-email-sender/Dockerfile @@ -1,7 +1,19 @@ 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 + 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 f4dc20a62..a58b218f8 100644 --- a/logger/Dockerfile +++ b/logger/Dockerfile @@ -1,7 +1,19 @@ 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 + USER node + +RUN yarn + +RUN yarn build + EXPOSE 3800 + ENTRYPOINT [ "node", "--max_old_space_size=8192", "dist" ] diff --git a/package.json b/package.json index 15589a2b8..1feb5351f 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", @@ -37,6 +38,7 @@ "runEsCommand": "ts-node ./src/commands/runEsCommand.ts", "trackTelemetry": "node ./dist/commands/trackTelemetry.js", "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": { @@ -89,10 +91,13 @@ "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", + "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", @@ -114,8 +119,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/__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/apolloClient.ts b/src/apolloClient.ts index 02d4aeca4..0cb1264fd 100644 --- a/src/apolloClient.ts +++ b/src/apolloClient.ts @@ -62,6 +62,15 @@ const apolloServer = new ApolloServer({ cookies: req.cookies, }; + if (user) { + Users.update( + { _id: user._id }, + { + lastSeenAt: new Date(), + }, + ).exec(); + } + if (USE_BRAND_RESTRICTIONS !== 'true') { return { brandIdSelector: {}, @@ -90,10 +99,33 @@ 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 } }; + commonQuerySelectorElk = { + bool: { + should: [ + { + terms: { + scopeBrandIds: scopeBrandIds.filter(c => typeof c === 'string'), + }, + }, + { + bool: { + must_not: { + exists: { + field: 'scopeBrandIds', + }, + }, + }, + }, + ], + }, + }; userBrandIdsSelector = { brandIds: { $in: scopeBrandIds } }; singleBrandIdSelector = { brandId: { $in: scopeBrandIds } }; } @@ -102,7 +134,10 @@ const apolloServer = new ApolloServer({ return { brandIdSelector, singleBrandIdSelector, - docModifier: doc => ({ ...doc, scopeBrandIds }), + docModifier: doc => ({ + ...doc, + scopeBrandIds: scopeBrandIds.filter((c: any) => c?.length), + }), commonQuerySelector, commonQuerySelectorElk, userBrandIdsSelector, diff --git a/src/commands/initFlow.ts b/src/commands/initFlow.ts new file mode 100644 index 000000000..85c154c77 --- /dev/null +++ b/src/commands/initFlow.ts @@ -0,0 +1,54 @@ +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.automation.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.define.owner', + '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 a26d32640..699fc1496 100644 --- a/src/commands/initProject.ts +++ b/src/commands/initProject.ts @@ -13,19 +13,25 @@ connect() }); // create admin user - const user = await Users.create({ - username: 'admin', - password: await Users.generatePassword(newPwd), - email: 'admin@erxes.io', - isOwner: true, - details: { - fullName: 'Admin', - }, - }); + let user = await Users.findOne({ isOwner: true }); + + 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); - console.log('\x1b[32m%s\x1b[0m', 'Your new password: ' + newPwd); + return Users.findOne({ _id: user._id }); + } - return Users.findOne({ _id: user._id }); + return; }) .then(() => { 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/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}

`, ); } diff --git a/src/data/constants.ts b/src/data/constants.ts index a95487817..87259877b 100644 --- a/src/data/constants.ts +++ b/src/data/constants.ts @@ -290,6 +290,9 @@ export const MODULE_NAMES = { ENGAGE: 'engage', SCRIPT: 'script', FIELD: 'field', + FLOW_ACTION_TYPE: 'flowActionType', + FLOW_ACTION: 'flowAction', + FLOW: 'flow', WEBHOOK: 'webhook', }; diff --git a/src/data/dataSources/integrations.ts b/src/data/dataSources/integrations.ts index a421d1695..239b68ac2 100644 --- a/src/data/dataSources/integrations.ts +++ b/src/data/dataSources/integrations.ts @@ -74,6 +74,10 @@ export default class IntegrationsAPI extends RESTDataSource { public async replyWhatsApp(params) { return this.post('/whatsapp/reply', params); } + + public async replyWhatsPro(params) { + return this.post('/whatsPro/reply', params); + } public async updateConfigs(configsMap) { return this.post('/update-configs', { configsMap }); 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/modules/coc/utils.ts b/src/data/modules/coc/utils.ts index d9b3ceb0e..ccd42c372 100644 --- a/src/data/modules/coc/utils.ts +++ b/src/data/modules/coc/utils.ts @@ -172,8 +172,19 @@ export class CommonBuilder { // filter by search value public searchFilter(value: string): void { this.positiveList.push({ - wildcard: { - searchText: `*${value.toLowerCase()}*`, + bool: { + should: [ + { + match_phrase: { + searchText: value, + }, + }, + { + wildcard: { + searchText: `*${value}*`, + }, + }, + ], }, }); } 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 new file mode 100644 index 000000000..ad82fcf8d --- /dev/null +++ b/src/data/modules/flow/index.ts @@ -0,0 +1,747 @@ +import * as moment from 'moment'; +import { + ConversationMessages, + Conversations, + Flows, + Integrations, + Users, + FlowActions, + Customers, + Channels, +} 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 { KIND_CHOICES } from '../../../db/models/definitions/constants'; +import { + IConversationMessageAdd, + publishConversationsChanged, + sendConversationToIntegrations, + publishMessage, +} from '../../resolvers/mutations/conversations'; +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']; + +const { RETURN_CHAT_TO_BOT_AFTER } = process.env; + +export function removeDiacritics(string) { + return String(string) + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); +} + +const checkIfIsCondition = ( + condition: IFlowActionValueCondition, + content: string = '', + response: any = null, + attachments: any[] = [], +) => { + content = removeDiacritics(content.toLowerCase()); + + switch (condition.operator) { + case '=': // Equals + switch (condition.type) { + case 'erxes.conditional.variable': + switch (condition.variable.key) { + case 'onboarding_active': + let now = moment.utc(); + now = now.utcOffset(-180); + let not = [0].includes(now.weekday()) || now.hour() < 9 || now.hour() > 18; + return (condition.variable.value === '0' && not) || (condition.variable.value === '1' && !not); + + default: + break; + } + break; + + default: + return ( + (response && condition.values.includes(response.id)) || + condition.values.map(c => removeDiacritics(c.toLowerCase())).includes(content) + ); + } + + 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 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 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; + + let conversation = await Conversations.getConversation(msg.conversationId); + + if (!conversation) return; + + const integration = await Integrations.getIntegration(conversation?.integrationId); + + if (!integration || !integration.flowId) return; + + const flow = await Flows.getFlow(integration.flowId); + + if (!flow) return; + + 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); + + 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], + user.id, + ); + + conversation = conversations[0]; + conversation.currentFlowActionId = undefined; + + // 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.findById(conversation.currentFlowActionId); + + if (flowAction) { + 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, + order: flowAction.order + 1, + }); + + // if (actionWithSendNext.includes(flowAction?.type || "")) + // sendNextMessage = true; + + break; + case 'erxes.action.execute.automation.flow': + flowAction = await FlowActions.findOne({ + flowId: flowAction?.value, + order: 0, + }); + + 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 || '{}'); + + const condition = conditions.find(c => checkIfIsCondition(c, msg.content, msg.response, msg.attachments)); + + if (condition) { + switch (condition.action) { + case 'erxes.action.execute.action': + flowAction = await FlowActions.findOne({ + flowId: flowAction.flowId, + order: condition.value, + }); + + break; + case 'erxes.action.transfer.to.agent': + await handleTransferToAgent(conversation, integration, condition, flowAction); + return; + default: + break; + } + } + break; + } + case 'erxes.action.conditional': { + const { conditions }: IFlowActionValue = JSON.parse(flowAction.value || '{}'); + + const condition = conditions.find(c => checkIfIsCondition(c, msg.content, msg.response, msg.attachments)); + + 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; + } + } + } + + if (!flowAction) { + if (integration?.flowId) { + flowAction = await FlowActions.findOne({ + flowId: flow.id, + order: 0, + }); + + if (actionWithSendNext.includes(flowAction?.type || '')) sendNextMessage = true; + } + } + + if (!flowAction) return; + + await Conversations.updateConversation(conversation.id, { + currentFlowActionId: flowAction.id, + }); + + switch (flowAction.type) { + case 'erxes.action.send.message': + await processSendMessage(flowAction, conversation); + + if ( + await FlowActions.findOne({ + flowId: flowAction.flowId, + order: flowAction.order + 1, + }) + ) + sendNextMessage = true; + + break; + case 'erxes.action.to.ask': + await processSendMessage(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.define.owner': + Customers.updateCustomer(conversation.customerId as string, { + ownerId: flowAction.value, + }); + + if ( + await FlowActions.findOne({ + flowId: flowAction.flowId, + order: flowAction.order + 1, + }) + ) + sendNextMessage = true; + + break; + case 'erxes.action.transfer.to.agent': + const condition = JSON.parse(flowAction.value || '{}'); + + handleTransferToAgent(conversation, integration, condition, flowAction); + + break; + case 'erxes.action.execute.automation.flow': + sendNextMessage = true; + + break; + + default: + break; + } + + if (sendNextMessage) { + await new Promise(res => setTimeout(res, 1000)); + await handleMessage(msg); + } +}; + +const handleTransferToAgent = async ( + conversation: IConversationDocument, + integration: IIntegrationDocument, + condition?: IFlowActionValueCondition, + flowAction?: IFlowActionDocument, +) => { + let assignedUserId: string = condition?.variable?.assignUser || ''; + + let channel = await Channels.findById(conversation.channelId); + + if (!assignedUserId && channel && channel.memberIds?.length) { + let users = await Users.aggregate([ + { + $match: { + _id: { $in: channel.memberIds }, + isActive: { $ne: false }, + 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 ? assignedUser.details?.toObject() : assignedUser.details) || {}; + + 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, + buttons: condition.buttons, + list: condition.list, + }, + assignedUser, + ); + } + } + } else if (condition!.error) { + let user: IUserDocument | null = null; + + if (channel && channel.memberIds?.length) { + let users = await Users.aggregate([ + { + $match: { + $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 } }, + ]); + + 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, + }); + + 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, + { + conversationId: conversation.id, + flowActionId: flowAction?.id, + internal: false, + content, + buttons: condition?.buttons, + list: condition?.list, + }, + user, + ); + } + } + } +}; + +const processSendMessage = async (flowAction: IFlowActionDocument, conversation: IConversationDocument) => { + const integration = await Integrations.getIntegration(conversation.integrationId); + + if (!integration) return; + + 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 || '[]'); + + let position = content.length - 1; + + position = Math.round(position * Math.random()); + + let message = content[position]; + + let file = ''; + let fileType = ''; + let text = ''; + let buttons = null; + let list = null; + + if (typeof message === 'string') { + text = message; + } else { + text = message.text; + file = message.file; + fileType = message.fileType; + buttons = message.buttons; + list = message.list; + } + + let lastMessage = await ConversationMessages.findOne({ + userId: conversation.assignedUserId, + conversationId: conversation.id, + }) + .sort({ createdAt: -1 }) + .exec(); + + if ( + lastMessage && + text && + text.includes(lastMessage.content || '') && + moment(lastMessage.createdAt).isAfter(moment().subtract(30, 'minutes')) + ) + return; + + const doc: IConversationMessageAdd = { + conversationId: conversation.id, + flowActionId: flowAction?.id, + internal: false, + content: text, + buttons, + list, + attachments: file ? [{ type: fileType, url: file }] : undefined, + }; + + 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; + + // 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.WHATSPRO) { + type = 'whatspro'; + requestName = 'replyWhatsPro'; + } + + await sendConversationToIntegrations( + type, + integrationId, + conversationId, + requestName, + doc, + { IntegrationsAPI: new IntegrationsAPI() }, + action, + message._id, + ); + + const dbMessage = await ConversationMessages.getMessage(message._id); + + // Publishing both admin & client + publishMessage(dbMessage, conversation.customerId); +}; + +export default { + handleMessage, +}; diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts index 9d73029d2..04e6e3254 100644 --- a/src/data/modules/integrations/receiveMessage.ts +++ b/src/data/modules/integrations/receiveMessage.ts @@ -9,6 +9,7 @@ import { 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', @@ -54,6 +55,7 @@ export const receiveRpcMessage = async msg => { } if (customer) { + await Customers.updateCustomer(customer._id, doc); return sendSuccess({ _id: customer._id }); } else { customer = await Customers.createCustomer({ @@ -76,23 +78,53 @@ 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 }); } 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; + + if (!userId) { + const user = await Users.findOne({ isOwner: true }); + + userId = user?._id; + } + } + + doc.userId = userId; + } + 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, @@ -118,9 +150,41 @@ export const receiveRpcMessage = async msg => { conversationMessageInserted: message, }); + flow.handleMessage(message); + 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; + + const update: any = { status: message.status }; + + if (doc.createdAt) update.createdAt = doc.createdAt; + + await ConversationMessages.updateOne({ _id: message._id }, update); + + const conversationDoc: { + updatedAt?: Date; + } = {}; + + if (doc.createdAt) { + conversationDoc.updatedAt = doc.createdAt; + } + + await Conversations.updateConversation(message.conversationId, conversationDoc); + + graphqlPubsub.publish('conversationMessageUpdated', { + conversationMessageUpdated: message, + }); + + return sendSuccess({ _id: doc.id }); + } + if (action === 'get-configs') { const configs = await getConfigs(); return sendSuccess({ configs }); 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 95a93cc67..6ed0462db 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, @@ -86,6 +89,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 5dee2af08..88c883a85 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, Tags } from '../../../db/models'; +import { ConversationMessages, Conversations, Customers, Integrations, Users, Tags } from '../../../db/models'; import Messages from '../../../db/models/ConversationMessages'; import { KIND_CHOICES, @@ -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'; @@ -26,6 +27,9 @@ export interface IConversationMessageAdd { mentionedUserIds?: string[]; internal?: boolean; attachments?: any; + buttons?: any; + list?: any; + flowActionId?: string; } interface IReplyFacebookComment { @@ -38,7 +42,7 @@ interface IReplyFacebookComment { * Send conversation to integrations */ -const sendConversationToIntegrations = ( +export const sendConversationToIntegrations = ( type: string, integrationId: string, conversationId: string, @@ -46,6 +50,8 @@ const sendConversationToIntegrations = ( doc: IConversationMessageAdd, dataSources: any, action?: string, + messageId?: string, + customer?: ICustomerDocument | null, ) => { if (type === 'facebook') { const regex = new RegExp(']* src="([^"]*)"', 'g'); @@ -70,12 +76,23 @@ 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, integrationId, + messageId, content: strip(doc.content), attachments: doc.attachments || [], + buttons: doc.buttons, + list: doc.list, + customer, }); } }; @@ -203,6 +220,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 */ @@ -311,7 +340,21 @@ const conversationMutations = { requestName = 'replyWhatsApp'; } - await sendConversationToIntegrations(type, integrationId, conversationId, requestName, doc, dataSources, action); + if (kind === KIND_CHOICES.WHATSPRO) { + requestName = 'replyWhatsPro'; + } + + await sendConversationToIntegrations( + type, + integrationId, + conversationId, + requestName, + doc, + dataSources, + action, + message._id, + customer, + ); const dbMessage = await ConversationMessages.getMessage(message._id); @@ -381,6 +424,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; }, @@ -400,6 +458,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/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 81d02ede3..b1f2afad8 100644 --- a/src/data/resolvers/mutations/index.ts +++ b/src/data/resolvers/mutations/index.ts @@ -33,6 +33,9 @@ import tickets from './tickets'; import users from './users'; import webhooks from './webhooks'; import widgets from './widgets'; +import flowActionTypes from './flowActionTypes'; +import flowActions from './flowActions'; +import flows from './flows'; export default { ...users, @@ -71,5 +74,8 @@ export default { ...checklists, ...robot, ...widgets, + ...flowActionTypes, + ...flowActions, + ...flows, ...webhooks, }; diff --git a/src/data/resolvers/mutations/integrations.ts b/src/data/resolvers/mutations/integrations.ts index 40ceac4c2..4f9c489f0 100644 --- a/src/data/resolvers/mutations/integrations.ts +++ b/src/data/resolvers/mutations/integrations.ts @@ -251,6 +251,7 @@ const integrationMutations = { 'smooch-line', 'smooch-twilio', 'whatsapp', + 'whatspro', 'telnyx', ].includes(integration.kind) ) { diff --git a/src/data/resolvers/queries/activityLogs.ts b/src/data/resolvers/queries/activityLogs.ts index 25bc939c6..1e24c5760 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', ); 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 7a8475bef..34321c666 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[]; } @@ -13,6 +15,7 @@ interface IExists { export interface IListArgs { limit?: number; + customerId?: string; channelId?: string; status?: string; unassigned?: string; @@ -20,6 +23,7 @@ export interface IListArgs { brandId?: string; tag?: string; integrationType?: string; + integrationId?: string; participating?: string; starred?: string; ids?: string[]; @@ -30,6 +34,7 @@ export interface IListArgs { interface IUserArgs { _id: string; + isOwner?: boolean; starredConversationIds?: string[]; } @@ -70,16 +75,24 @@ 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 $or: [ { userId: { $exists: true }, - messageCount: { $gt: 1 }, + messageCount: { $gte: 1 }, + ...assignedUserQuery, }, { userId: { $exists: false }, + ...assignedUserQuery, }, ], }; @@ -143,7 +156,16 @@ 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)), + }, + }; + } + + // filter by customer + public async customerFilter(customerId: string): Promise<{ customerId: string }> { + return { + customerId, }; } @@ -197,7 +219,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: [ @@ -237,6 +261,7 @@ export default class Builder { unassigned: {}, tag: {}, channel: {}, + costumer: {}, integrationType: {}, // find it using channel && brand @@ -251,9 +276,18 @@ 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(); + if (!this.queries.integrations.integrationId.$in.length) { + this.queries.integrations = {}; + } + // unassigned if (this.params.unassigned) { this.queries.unassigned = this.unassignedFilter(); @@ -297,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/resolvers/queries/conversations.ts b/src/data/resolvers/queries/conversations.ts index 068c68ffb..ec1a3522a 100644 --- a/src/data/resolvers/queries/conversations.ts +++ b/src/data/resolvers/queries/conversations.ts @@ -93,12 +93,14 @@ const conversationQueries = { // initiate query builder const qb = new QueryBuilder(params, { _id: user._id, + isOwner: user.isOwner, starredConversationIds: user.starredConversationIds, }); await qb.buildAllQueries(); - return Conversations.find(qb.mainQuery()) + const query = qb.mainQuery(); + return Conversations.find(query) .sort({ updatedAt: -1 }) .limit(params.limit || 0); }, @@ -189,6 +191,7 @@ const conversationQueries = { const qb = new QueryBuilder(params, { _id: user._id, + isOwner: user.isOwner, starredConversationIds: user.starredConversationIds, }); @@ -267,6 +270,7 @@ const conversationQueries = { // initiate query builder const qb = new QueryBuilder(params, { _id: user._id, + isOwner: user.isOwner, starredConversationIds: user.starredConversationIds, }); @@ -282,6 +286,7 @@ const conversationQueries = { // initiate query builder const qb = new QueryBuilder(params, { _id: user._id, + isOwner: user.isOwner, starredConversationIds: user.starredConversationIds, }); @@ -295,7 +300,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/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': 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 ef73d0e47..a7f9eea59 100644 --- a/src/data/resolvers/queries/index.ts +++ b/src/data/resolvers/queries/index.ts @@ -36,6 +36,9 @@ import tickets from './tickets'; import users from './users'; import webhooks from './webhooks'; import widgets from './widgets'; +import flowActionTypes from './flowActionTypes'; +import flowActions from './flowActions'; +import flows from './flows'; export default { ...users, @@ -77,5 +80,8 @@ export default { ...robot, ...pipelineLabels, ...widgets, + ...flowActionTypes, + ...flowActions, + ...flows, ...webhooks, }; diff --git a/src/data/resolvers/queries/logs.ts b/src/data/resolvers/queries/logs.ts index ba56c0dc6..095362972 100644 --- a/src/data/resolvers/queries/logs.ts +++ b/src/data/resolvers/queries/logs.ts @@ -33,6 +33,9 @@ import { tagSchema } from '../../../db/models/definitions/tags'; import { taskSchema } from '../../../db/models/definitions/tasks'; import { ticketSchema } from '../../../db/models/definitions/tickets'; import { userSchema } from '../../../db/models/definitions/users'; +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.USER, schemas: [userSchema], }, + { + name: MODULE_NAMES.FLOW, + schemas: [flowSchema], + }, + { + name: MODULE_NAMES.FLOW_ACTION_TYPE, + schemas: [flowActionTypeSchema], + }, + { + name: MODULE_NAMES.FLOW_ACTION, + schemas: [flowActionSchema], + }, ]; /** diff --git a/src/data/resolvers/subscriptions/conversations.ts b/src/data/resolvers/subscriptions/conversations.ts index a16466c38..7100a2380 100644 --- a/src/data/resolvers/subscriptions/conversations.ts +++ b/src/data/resolvers/subscriptions/conversations.ts @@ -30,6 +30,19 @@ export default { }, /* + * Listen for message update + */ + conversationMessageUpdated: { + subscribe: withFilter( + () => graphqlPubsub.asyncIterator('conversationMessageUpdated'), + // filter by conversationId + (payload, variables) => { + return payload.conversationMessageUpdated.conversationId === variables._id; + }, + ), + }, + + /* * Show typing while waiting Bot response */ conversationBotTypingStatus: { diff --git a/src/data/schema/conversation.ts b/src/data/schema/conversation.ts index e77833d4d..7060bd053 100644 --- a/src/data/schema/conversation.ts +++ b/src/data/schema/conversation.ts @@ -37,6 +37,7 @@ export const types = ` participatedUsers: [User] participatorCount: Int videoCallData: VideoCallData + currentFlowActionId: String productBoardLink: String } @@ -60,12 +61,14 @@ export const types = ` type ConversationMessage { _id: String! content: String + status: String attachments: [Attachment] mentionedUserIds: [String] conversationId: String internal: Boolean fromBot: Boolean botData: JSON + isGroupMsg: Boolean customerId: String userId: String createdAt: Date @@ -182,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 @@ -234,6 +239,11 @@ export const queries = ` `; export const mutations = ` + conversationAdd( + customerId: String, + integrationId: String, + content: String + ): Conversation conversationMessageAdd( conversationId: String, content: String, diff --git a/src/data/schema/flow.ts b/src/data/schema/flow.ts new file mode 100644 index 000000000..5d6c9b05e --- /dev/null +++ b/src/data/schema/flow.ts @@ -0,0 +1,30 @@ +export const types = ` + type Flow { + _id: String! + name: String + description: String + actions: [FlowAction] + integrations: [Integration] + assignedUserId: String + createdAt: Date + type: String + watsonApiKey: String + watsonAssistantId: String + watsonSkillId: String + watsonAssistantUrl: String + } +`; + +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..4f6840cae --- /dev/null +++ b/src/data/schema/flowAction.ts @@ -0,0 +1,25 @@ +export const types = ` + type FlowAction { + _id: String! + actionId: String! + type: String! + value: String! + order: Int! + flowId: String! + createdAt: Date + executeNext: Boolean! + } +`; + +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 ff3d14770..6db801667 100755 --- a/src/data/schema/index.ts +++ b/src/data/schema/index.ts @@ -125,6 +125,14 @@ import { import { mutations as WebhookMutations, queries as WebhookQueries, types as WebhookTypes } from './webhook'; 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 @@ -168,6 +176,9 @@ export const types = ` ${RobotTypes} ${PipelineLabelTypes} ${WidgetTypes} + ${FlowActionTypeTypes} + ${FlowActionTypes} + ${FlowTypes} ${WebhookTypes} `; @@ -210,6 +221,9 @@ export const queries = ` ${RobotQueries} ${PipelineLabelQueries} ${WidgetQueries} + ${FlowActionTypeQueries} + ${FlowActionQueries} + ${FlowQueries} ${WebhookQueries} } `; @@ -251,6 +265,9 @@ export const mutations = ` ${RobotMutations} ${PipelineLabelMutations} ${WidgetMutations} + ${FlowActionTypeMutations} + ${FlowActionMutations} + ${FlowMutations} ${WebhookMutations} } `; @@ -259,6 +276,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 diff --git a/src/data/schema/integration.ts b/src/data/schema/integration.ts index 29ecb7578..b7fe74217 100644 --- a/src/data/schema/integration.ts +++ b/src/data/schema/integration.ts @@ -19,6 +19,8 @@ export const types = ` form: Form channels: [Channel] + flowId: String + websiteMessengerApps: [MessengerApp] knowledgeBaseMessengerApps: [MessengerApp] leadMessengerApps: [MessengerApp] @@ -102,7 +104,8 @@ export const queries = ` searchValue: String, channelId: String, brandId: String, - tag: String + tag: String, + flowId: String ): [Integration] integrationsGetUsedTypes: [integrationsGetUsedTypes] diff --git a/src/data/schema/user.ts b/src/data/schema/user.ts index bac312a05..11fe4c714 100644 --- a/src/data/schema/user.ts +++ b/src/data/schema/user.ts @@ -50,6 +50,7 @@ export const types = ` permissionActions: JSON configs: JSON configsConstants: [JSON] + lastSeenAt: Date onboardingHistory: OnboardingHistory } diff --git a/src/data/types.ts b/src/data/types.ts index a2a8d554b..139f58dfd 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/data/utils.ts b/src/data/utils.ts index 52ae71917..542cdb9bf 100644 --- a/src/data/utils.ts +++ b/src/data/utils.ts @@ -79,19 +79,9 @@ export const checkFile = async (file, source?: string) => { return 'ok'; } - 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'); - 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'; } diff --git a/src/db/factories.ts b/src/db/factories.ts index 37b00e736..c472c35d9 100644 --- a/src/db/factories.ts +++ b/src/db/factories.ts @@ -49,6 +49,9 @@ import { Tickets, Users, UsersGroups, + FlowActionTypes, + FlowActions, + Flows, Webhooks, } from './models'; import { ICustomField } from './models/definitions/common'; @@ -1352,6 +1355,54 @@ export function engageDataFactory(params: IMessageEngageDataParams) { }; } +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(); +}; + interface IWebhookActionInput { label?: string; type?: string; @@ -1384,4 +1435,4 @@ export const onboardHistoryFactory = async (params: IOnboardHistoryParams) => { const onboard = new OnboardingHistories(params); return onboard.save(); -}; +}; \ No newline at end of file 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..f99e87463 --- /dev/null +++ b/src/db/models/FlowActions.ts @@ -0,0 +1,66 @@ +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 + + 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(), + }); + } + + 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/constants.ts b/src/db/models/definitions/constants.ts index 3e1a52494..3b65fd83b 100644 --- a/src/db/models/definitions/constants.ts +++ b/src/db/models/definitions/constants.ts @@ -86,6 +86,7 @@ export const KIND_CHOICES = { SMOOCH_TELEGRAM: 'smooch-telegram', SMOOCH_TWILIO: 'smooch-twilio', WHATSAPP: 'whatsapp', + WHATSPRO: 'whatspro', TELNYX: 'telnyx', WEBHOOK: 'webhook', ALL: [ @@ -108,6 +109,7 @@ export const KIND_CHOICES = { 'smooch-telegram', 'smooch-twilio', 'whatsapp', + 'whatspro', 'telnyx', 'webhook', ], @@ -133,6 +135,7 @@ export const INTEGRATION_NAMES_MAP = { 'smooch-telegram': 'Telegram', 'smooch-twilio': 'Twilio SMS', whatsapp: 'WhatsApp', + whatspro: 'WhatsPro', webhook: 'Webhook', }; @@ -409,7 +412,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 diff --git a/src/db/models/definitions/conversationMessages.ts b/src/db/models/definitions/conversationMessages.ts index fbd497e91..fe37497fa 100644 --- a/src/db/models/definitions/conversationMessages.ts +++ b/src/db/models/definitions/conversationMessages.ts @@ -29,18 +29,24 @@ export interface IMessage { content?: string; createdAt?: Date; attachments?: any; + buttons?: any; + list?: any; + response?: any; mentionedUserIds?: string[]; conversationId: string; internal?: boolean; customerId?: string; userId?: string; fromBot?: boolean; + isGroupMsg?: boolean; isCustomerRead?: boolean; formWidgetData?: any; botData?: any; messengerAppData?: any; engageData?: IEngageData; contentType?: string; + flowActionId?: string; + status?: string; } export interface IMessageDocument extends IMessage, Document { @@ -59,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: String }), + 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 }), @@ -87,11 +129,15 @@ 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 }), 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 }), @@ -104,4 +150,6 @@ export const messageSchema = new Schema({ enum: MESSAGE_TYPES.ALL, default: MESSAGE_TYPES.TEXT, }), + flowActionId: field({ type: String, index: true }), + status: field({ type: String }), }); diff --git a/src/db/models/definitions/conversations.ts b/src/db/models/definitions/conversations.ts index bd6b01ea0..cd24ec8ee 100644 --- a/src/db/models/definitions/conversations.ts +++ b/src/db/models/definitions/conversations.ts @@ -27,6 +27,9 @@ export interface IConversation { firstRespondedUserId?: string; firstRespondedDate?: Date; + currentFlowActionId?: string; + channelId?: string; + isCustomerRespondedLast?: boolean; } @@ -79,5 +82,9 @@ export const conversationSchema = new Schema({ firstRespondedUserId: field({ type: String }), firstRespondedDate: field({ type: Date }), + currentFlowActionId: field({ type: String }), + + channelId: field({ type: String }), + isCustomerRespondedLast: field({ type: Boolean }), }); 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..87c47392d --- /dev/null +++ b/src/db/models/definitions/flowActions.ts @@ -0,0 +1,59 @@ +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; + executeNext?: boolean; +} + +export interface IFlowActionDocument extends IFlowAction, Document { + _id: string; + createdAt: Date; +} + +export interface IFlowActionValueCondition { + operator: string; + type: string; + values: string[]; + action: string; + value: string; + variable: any; + error: string; + buttons: any; + list: any; +} + +export interface IFlowActionContent { + file: string; + fileType: string; + text: string; + buttons: any; + list: any; +} + +export interface IFlowActionValue { + content: string[] | IFlowActionContent[]; + 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, 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' }), +}); diff --git a/src/db/models/definitions/flows.ts b/src/db/models/definitions/flows.ts new file mode 100644 index 000000000..d120d9eaa --- /dev/null +++ b/src/db/models/definitions/flows.ts @@ -0,0 +1,34 @@ +import { Document, Schema } from 'mongoose'; +import { field } from './utils'; + +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 { + _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' }), + 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/src/db/models/definitions/integrations.ts b/src/db/models/definitions/integrations.ts index 7504dbe83..6b6c047fb 100644 --- a/src/db/models/definitions/integrations.ts +++ b/src/db/models/definitions/integrations.ts @@ -110,12 +110,14 @@ export interface IIntegration { messengerData?: IMessengerData; uiOptions?: IUiOptions; isActive?: boolean; + flowId?: string; channelIds?: string[]; } export interface IIntegrationDocument extends IIntegration, Document { _id: string; createdUserId: string; + defaultSenderId: string; // TODO remove formData?: ILeadData; leadData?: ILeadDataDocument; @@ -300,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, @@ -319,6 +321,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' }), webhookData: field({ type: webhookDataSchema }), // TODO: remove formData: field({ type: leadDataSchema }), diff --git a/src/db/models/definitions/users.ts b/src/db/models/definitions/users.ts index 6032a22b9..09f1772f5 100644 --- a/src/db/models/definitions/users.ts +++ b/src/db/models/definitions/users.ts @@ -37,6 +37,7 @@ export interface IUser { details?: IDetail; links?: ILink; isActive?: boolean; + lastSeenAt?: number; brandIds?: string[]; groupIds?: string[]; deviceTokens?: string[]; @@ -67,7 +68,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 }, ); @@ -92,14 +97,29 @@ export const userSchema = new Schema({ match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,10})+$/, '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: Object, 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 }), }); diff --git a/src/db/models/index.ts b/src/db/models/index.ts index cd9baed6a..dfe55ce1f 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'; import Webhooks from './Webhook'; export { @@ -83,6 +86,9 @@ export { PipelineLabels, Checklists, ChecklistItems, + FlowActionTypes, + FlowActions, + Flows, OnboardingHistories, Webhooks, }; diff --git a/src/elasticsearch.ts b/src/elasticsearch.ts index 4b52bf5da..0a4a3065a 100644 --- a/src/elasticsearch.ts +++ b/src/elasticsearch.ts @@ -23,7 +23,7 @@ export const getIndexPrefix = () => { } const uriObject = mongoUri.parse(MONGO_URL); - const dbName = uriObject.database; + const dbName = uriObject.database.toLowerCase(); return `${dbName}__`; }; 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 => { diff --git a/yarn.lock b/yarn.lock index a5c2bacf0..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== @@ -6376,6 +6675,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" @@ -6971,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" @@ -6978,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" @@ -7246,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" @@ -7282,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" @@ -7464,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= @@ -7866,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" @@ -7963,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" @@ -8037,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" @@ -8140,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" @@ -8556,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" @@ -9094,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" @@ -9539,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" @@ -9756,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" @@ -9835,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" @@ -9906,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" @@ -9927,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" @@ -10052,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" @@ -10297,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"