diff --git a/apps/meteor/app/livechat/imports/server/rest/visitors.js b/apps/meteor/app/livechat/imports/server/rest/visitors.js index f063e6c19588..1bb8fba375e6 100644 --- a/apps/meteor/app/livechat/imports/server/rest/visitors.js +++ b/apps/meteor/app/livechat/imports/server/rest/visitors.js @@ -8,7 +8,7 @@ import { findChatHistory, searchChats, findVisitorsToAutocomplete, - findVisitorsByEmailOrPhoneOrNameOrUsername, + findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField, } from '../../../server/api/lib/visitors'; API.v1.addRoute( @@ -144,7 +144,7 @@ API.v1.addRoute( const nameOrUsername = new RegExp(escapeRegExp(term), 'i'); return API.v1.success( - await findVisitorsByEmailOrPhoneOrNameOrUsername({ + await findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField({ userId: this.userId, emailOrPhone: term, nameOrUsername, diff --git a/apps/meteor/app/livechat/server/api/lib/rooms.js b/apps/meteor/app/livechat/server/api/lib/rooms.js index 32862d65b4dd..ed5db48e9257 100644 --- a/apps/meteor/app/livechat/server/api/lib/rooms.js +++ b/apps/meteor/app/livechat/server/api/lib/rooms.js @@ -35,7 +35,7 @@ export async function findRooms({ const departmentsIds = [...new Set(rooms.map((room) => room.departmentId).filter(Boolean))]; if (departmentsIds.length) { const departments = await LivechatDepartment.findInIds(departmentsIds, { - fields: { name: 1 }, + projection: { name: 1 }, }).toArray(); rooms.forEach((room) => { diff --git a/apps/meteor/app/livechat/server/api/lib/visitors.js b/apps/meteor/app/livechat/server/api/lib/visitors.js index d787c15c376c..cfa62ead68d6 100644 --- a/apps/meteor/app/livechat/server/api/lib/visitors.js +++ b/apps/meteor/app/livechat/server/api/lib/visitors.js @@ -1,4 +1,4 @@ -import { LivechatVisitors, Messages, LivechatRooms } from '@rocket.chat/models'; +import { LivechatVisitors, Messages, LivechatRooms, LivechatCustomField } from '@rocket.chat/models'; import { canAccessRoomAsync } from '../../../../authorization/server/functions/canAccessRoom'; @@ -129,20 +129,33 @@ export async function findVisitorsToAutocomplete({ selector }) { }; } -export async function findVisitorsByEmailOrPhoneOrNameOrUsername({ emailOrPhone, nameOrUsername, pagination: { offset, count, sort } }) { - const { cursor, totalCount } = LivechatVisitors.findPaginatedVisitorsByEmailOrPhoneOrNameOrUsername(emailOrPhone, nameOrUsername, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - projection: { - username: 1, - name: 1, - phone: 1, - livechatData: 1, - visitorEmails: 1, - lastChat: 1, +export async function findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField({ + emailOrPhone, + nameOrUsername, + pagination: { offset, count, sort }, +}) { + const allowedCF = await LivechatCustomField.findMatchingCustomFields('visitor', true, { projection: { _id: 1 } }) + .map((cf) => cf._id) + .toArray(); + + const { cursor, totalCount } = await LivechatVisitors.findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField( + emailOrPhone, + nameOrUsername, + allowedCF, + { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + projection: { + username: 1, + name: 1, + phone: 1, + livechatData: 1, + visitorEmails: 1, + lastChat: 1, + }, }, - }); + ); const [visitors, total] = await Promise.all([cursor.toArray(), totalCount]); diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index 8f6955e86484..f7b9a0b782b6 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -1,6 +1,7 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { LivechatVisitors } from '@rocket.chat/models'; +import { LivechatCustomField, LivechatVisitors } from '@rocket.chat/models'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; import { API } from '../../../../api/server'; import { Contacts } from '../../lib/Contacts'; @@ -47,23 +48,36 @@ API.v1.addRoute( check(this.queryParams, { email: Match.Maybe(String), phone: Match.Maybe(String), + custom: Match.Maybe(String), }); + const { email, phone, custom } = this.queryParams; - const { email, phone } = this.queryParams; + let customCF: { [k: string]: string } = {}; + try { + customCF = custom && JSON.parse(custom); + } catch (e) { + throw new Meteor.Error('error-invalid-params-custom'); + } - if (!email && !phone) { + if (!email && !phone && !Object.keys(customCF).length) { throw new Meteor.Error('error-invalid-params'); } - const query = Object.assign( - {}, - { - ...(email && { visitorEmails: { address: email } }), - ...(phone && { phone: { phoneNumber: phone } }), - }, - ); + const foundCF = await (async () => { + if (!custom) { + return {}; + } + + const cfIds = Object.keys(customCF); + + const customFields = await LivechatCustomField.findMatchingCustomFieldsByIds(Object.keys(cfIds), 'visitor', true, { + projection: { _id: 1 }, + }).toArray(); + + return Object.fromEntries(customFields.map(({ _id }) => [`livechatData.${_id}`, new RegExp(escapeRegExp(customCF[_id]), 'i')])); + })(); - const contact = await LivechatVisitors.findOne(query); + const contact = await LivechatVisitors.findOneByEmailAndPhoneAndCustomField(email, phone, foundCF); return API.v1.success({ contact }); }, }, diff --git a/apps/meteor/app/livechat/server/methods/saveCustomField.js b/apps/meteor/app/livechat/server/methods/saveCustomField.js index e22c1957c396..384f1502d60f 100644 --- a/apps/meteor/app/livechat/server/methods/saveCustomField.js +++ b/apps/meteor/app/livechat/server/methods/saveCustomField.js @@ -24,6 +24,7 @@ Meteor.methods({ scope: String, visibility: String, regexp: String, + searchable: Boolean, }), ); diff --git a/apps/meteor/app/models/server/models/LivechatRooms.js b/apps/meteor/app/models/server/models/LivechatRooms.js index b0ebd2369e05..a2c5239daf36 100644 --- a/apps/meteor/app/models/server/models/LivechatRooms.js +++ b/apps/meteor/app/models/server/models/LivechatRooms.js @@ -33,6 +33,7 @@ export class LivechatRooms extends Base { }, }, ); + this.tryEnsureIndex({ 'livechatData.$**': 1 }); } findOneByIdOrName(_idOrName, options) { diff --git a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsForm.js b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsForm.js index 2ade8bcbfc20..5ab6f4a19dfd 100644 --- a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsForm.js +++ b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsForm.js @@ -5,9 +5,9 @@ import React, { useMemo } from 'react'; const CustomFieldsForm = ({ values = {}, handlers = {}, className }) => { const t = useTranslation(); - const { id, field, label, scope, visibility, regexp } = values; + const { id, field, label, scope, visibility, searchable, regexp } = values; - const { handleField, handleLabel, handleScope, handleVisibility, handleRegexp } = handlers; + const { handleField, handleLabel, handleScope, handleVisibility, handleSearchable, handleRegexp } = handlers; const scopeOptions = useMemo( () => [ @@ -45,6 +45,14 @@ const CustomFieldsForm = ({ values = {}, handlers = {}, className }) => { + + + {t('Searchable')} + + + + + {t('Validation')} diff --git a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsRoute.js b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsRoute.js index 208035d08979..ae6c377db603 100644 --- a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsRoute.js +++ b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsRoute.js @@ -16,7 +16,6 @@ const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1); const useQuery = ({ text, itemsPerPage, current }, [column, direction]) => useMemo( () => ({ - fields: JSON.stringify({ label: 1 }), text, sort: JSON.stringify({ [column]: sortDir(direction) }), ...(itemsPerPage && { count: itemsPerPage }), diff --git a/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsPage.js b/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsPage.js index 89fdf1cff012..a93f5597af45 100644 --- a/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsPage.js +++ b/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsPage.js @@ -14,6 +14,7 @@ const getInitialValues = (cf) => ({ label: cf.label, scope: cf.scope, visibility: cf.visibility === 'visible', + searchable: !!cf.searchable, regexp: cf.regexp, }); diff --git a/apps/meteor/client/views/omnichannel/customFields/NewCustomFieldsPage.js b/apps/meteor/client/views/omnichannel/customFields/NewCustomFieldsPage.js index b4d4dcd4a447..2762e80eee39 100644 --- a/apps/meteor/client/views/omnichannel/customFields/NewCustomFieldsPage.js +++ b/apps/meteor/client/views/omnichannel/customFields/NewCustomFieldsPage.js @@ -14,6 +14,7 @@ const initialValues = { scope: 'visitor', visibility: true, regexp: '', + searchable: true, }; const NewCustomFieldsPage = ({ reload }) => { diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 4e2fe2e3f1e1..c7efa0695b01 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -4128,6 +4128,7 @@ "Script": "Script", "Script_Enabled": "Script Enabled", "Search": "Search", + "Searchable": "Searchable", "Search_Apps": "Search Apps", "Search_by_file_name": "Search by file name", "Search_by_username": "Search by username", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index d228dbde9d59..7b704e9c4faf 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -3822,6 +3822,7 @@ "Screen_Share": "Compartilhamento de tela", "Script_Enabled": "Script ativado", "Search": "Pesquisar", + "Searchable": "Pesquisável", "Search_Apps": "Pesquisar aplicativos", "Search_by_file_name": "Pesquisar por nome de arquivo", "Search_by_username": "Pesquisar por nome de usuário", diff --git a/apps/meteor/server/models/raw/BaseRaw.ts b/apps/meteor/server/models/raw/BaseRaw.ts index a45bdb822deb..cf4cc19c1c8d 100644 --- a/apps/meteor/server/models/raw/BaseRaw.ts +++ b/apps/meteor/server/models/raw/BaseRaw.ts @@ -159,7 +159,7 @@ export abstract class BaseRaw = undefined> impleme find(query?: Filter): FindCursor>; - find

(query: Filter, options: FindOptions

): FindCursor

; + find

(query: Filter, options?: FindOptions

): FindCursor

; find

(query: Filter | undefined = {}, options?: FindOptions

): FindCursor> | FindCursor> { const optionsDef = this.doNotMixInclusionAndExclusionFields(options); diff --git a/apps/meteor/server/models/raw/LivechatCustomField.ts b/apps/meteor/server/models/raw/LivechatCustomField.ts index a0402efca92f..be37341a8464 100644 --- a/apps/meteor/server/models/raw/LivechatCustomField.ts +++ b/apps/meteor/server/models/raw/LivechatCustomField.ts @@ -17,6 +17,34 @@ export class LivechatCustomFieldRaw extends BaseRaw implem return this.find({ scope }, options || {}); } + findMatchingCustomFields( + scope: ILivechatCustomField['scope'], + searchable = true, + options?: FindOptions, + ): FindCursor { + const query = { + scope, + searchable, + }; + + return this.find(query, options); + } + + findMatchingCustomFieldsByIds( + ids: ILivechatCustomField['_id'][], + scope: ILivechatCustomField['scope'], + searchable = true, + options?: FindOptions, + ): FindCursor { + const query = { + _id: { $in: ids }, + scope, + searchable, + }; + + return this.find(query, options); + } + async createOrUpdateCustomField( _id: string, field: string, diff --git a/apps/meteor/server/models/raw/LivechatVisitors.ts b/apps/meteor/server/models/raw/LivechatVisitors.ts index 601f344b5f6e..8a1fc40eec72 100644 --- a/apps/meteor/server/models/raw/LivechatVisitors.ts +++ b/apps/meteor/server/models/raw/LivechatVisitors.ts @@ -31,6 +31,7 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL { key: { name: 1 }, sparse: true }, { key: { username: 1 } }, { key: { 'contactMananger.username': 1 }, sparse: true }, + { key: { 'livechatData.$**': 1 } }, ]; } @@ -158,11 +159,12 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL /** * Find visitors by their email or phone or username or name */ - findPaginatedVisitorsByEmailOrPhoneOrNameOrUsername( + async findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField( emailOrPhone: string, nameOrUsername: RegExp, - options: FindOptions, - ): FindPaginated> { + allowedCustomFields: string[] = [], + options?: FindOptions, + ): Promise>> { const query = { $or: [ { @@ -177,12 +179,35 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL { username: nameOrUsername, }, + // nameorusername is a clean regex, so we should be good + ...allowedCustomFields.map((c: string) => ({ [`livechatData.${c}`]: nameOrUsername })), ], }; return this.findPaginated(query, options); } + async findOneByEmailAndPhoneAndCustomField( + email: string | null | undefined, + phone: string | null | undefined, + customFields?: { [key: string]: RegExp }, + ): Promise { + const query = Object.assign( + {}, + { + ...(email && { visitorEmails: { address: email } }), + ...(phone && { phone: { phoneNumber: phone } }), + ...customFields, + }, + ); + + if (Object.keys(query).length === 0) { + return null; + } + + return this.findOne(query); + } + async updateLivechatDataByToken( token: string, key: string, diff --git a/apps/meteor/tests/data/livechat/custom-fields.ts b/apps/meteor/tests/data/livechat/custom-fields.ts new file mode 100644 index 000000000000..ddae4d2b888f --- /dev/null +++ b/apps/meteor/tests/data/livechat/custom-fields.ts @@ -0,0 +1,52 @@ +import type { Response } from 'supertest'; +import { ILivechatCustomField } from '@rocket.chat/core-typings'; +import { credentials, request, methodCall, api } from './../api-data'; + +export const createCustomField = (customField: ILivechatCustomField) => new Promise((resolve, reject) => { + request + .get(api('livechat/custom-fields/'+customField.label)) + .set(credentials) + .send() + .end((err: Error, res:Response) => { + if (err) { + reject(err); + } else { + if (res.body.customField != null && res.body.customField != undefined) { + resolve(res.body.customField); + }else{ + request + .post(methodCall('livechat:saveCustomField')) + .send({ + message: JSON.stringify({ + method: 'livechat:saveCustomField', + params: [null,customField], + id: 'id', + msg: 'method', + }), + }) + .set(credentials) + .end((err: Error, res: Response): void => { + if (err) { + return reject(err); + } + resolve(res.body); + }); + } + } + }); + +}); + +export const deleteCustomField = (customFieldID: string) => new Promise((resolve, reject) => { + request + .post(methodCall('livechat:saveCustomField')) + .send(customFieldID) + .set(credentials) + .end((err: Error, res: Response): void => { + if (err) { + return reject(err); + } + resolve(res.body); + }); +}); + diff --git a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts index 8f4ff9df4fdc..1041ef45e35e 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts @@ -1,12 +1,13 @@ /* eslint-env mocha */ import { expect } from 'chai'; -import type { ILivechatAgent, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { ILivechatAgent, ILivechatCustomField, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; import { makeAgentAvailable, createAgent, createLivechatRoom, createVisitor, takeInquiry } from '../../../data/livechat/rooms'; +import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields'; describe('LIVECHAT - visitors', function () { this.retries(0); @@ -31,28 +32,34 @@ describe('LIVECHAT - visitors', function () { describe('livechat/visitors.info', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { - updatePermission('view-l-room', []).then(() => { - request - .get(api('livechat/visitors.info?visitorId=invalid')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(403) - .end(done); - }); + updatePermission('view-l-room', []) + .then(() => { + request + .get(api('livechat/visitors.info?visitorId=invalid')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-authorized'); + }); + }) + .then(() => done()); }); it('should return an "visitor not found error" when the visitor doe snot exists', (done) => { - updatePermission('view-l-room', ['admin']).then(() => { - request - .get(api('livechat/visitors.info?visitorId=invalid')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res: Response) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('visitor-not-found'); - }) - .end(done); - }); + updatePermission('view-l-room', ['admin']) + .then(() => { + request + .get(api('livechat/visitors.info?visitorId=invalid')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('visitor-not-found'); + }); + }) + .then(() => done()); }); it('should return the visitor info', (done) => { request @@ -70,46 +77,53 @@ describe('LIVECHAT - visitors', function () { describe('livechat/visitors.pagesVisited', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { - updatePermission('view-l-room', []).then(() => { - request - .get(api('livechat/visitors.pagesVisited/room-id')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(403) - .end(done); - }); + updatePermission('view-l-room', []) + .then(() => { + request + .get(api('livechat/visitors.pagesVisited/room-id')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-authorized'); + }); + }) + .then(() => done()); }); it('should return an "error" when the roomId param is not provided', (done) => { - updatePermission('view-l-room', ['admin']).then(() => { - request - .get(api('livechat/visitors.pagesVisited/room-id')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res: Response) => { - expect(res.body).to.have.property('success', false); - }) - .end(done); - }); + updatePermission('view-l-room', ['admin']) + .then(() => { + request + .get(api('livechat/visitors.pagesVisited/room-id')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + }); + }) + .then(() => done()); }); it('should return an array of pages', (done) => { updatePermission('view-l-room', ['admin']).then(() => { createVisitor().then((createdVisitor: ILivechatVisitor) => { - createLivechatRoom(createdVisitor.token).then((createdRoom: IOmnichannelRoom) => { - request - .get(api(`livechat/visitors.pagesVisited/${createdRoom._id}`)) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res: Response) => { - expect(res.body).to.have.property('success', true); - expect(res.body.pages).to.be.an('array'); - expect(res.body).to.have.property('offset'); - expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('count'); - }) - .end(done); - }); + createLivechatRoom(createdVisitor.token) + .then((createdRoom: IOmnichannelRoom) => { + request + .get(api(`livechat/visitors.pagesVisited/${createdRoom._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.pages).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }); + }) + .then(() => done()); }); }); }); @@ -117,47 +131,54 @@ describe('LIVECHAT - visitors', function () { describe('livechat/visitors.chatHistory/room/room-id/visitor/visitor-id', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { - updatePermission('view-l-room', []).then(() => { - request - .get(api('livechat/visitors.chatHistory/room/room-id/visitor/visitor-id')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(403) - .end(done); - }); + updatePermission('view-l-room', []) + .then(() => { + request + .get(api('livechat/visitors.chatHistory/room/room-id/visitor/visitor-id')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-authorized'); + }); + }) + .then(() => done()); }); it('should return an "error" when the roomId param is invalid', (done) => { - updatePermission('view-l-room', ['admin']).then(() => { - request - .get(api('livechat/visitors.chatHistory/room/room-id/visitor/visitor-id')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res: Response) => { - expect(res.body).to.have.property('success', false); - }) - .end(done); - }); + updatePermission('view-l-room', ['admin']) + .then(() => { + request + .get(api('livechat/visitors.chatHistory/room/room-id/visitor/visitor-id')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + }); + }) + .then(() => done()); }); it('should return an array of chat history', (done) => { updatePermission('view-l-room', ['admin']).then(() => { - createVisitor().then((createdVisitor: ILivechatVisitor) => { - createLivechatRoom(createdVisitor.token).then((createdRoom: IOmnichannelRoom) => { - request - .get(api(`livechat/visitors.chatHistory/room/${createdRoom._id}/visitor/${createdVisitor._id}`)) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res: Response) => { - expect(res.body).to.have.property('success', true); - expect(res.body.history).to.be.an('array'); - expect(res.body).to.have.property('offset'); - expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('count'); - }) - .end(done); - }); - }); + createVisitor() + .then((createdVisitor: ILivechatVisitor) => { + createLivechatRoom(createdVisitor.token).then((createdRoom: IOmnichannelRoom) => { + request + .get(api(`livechat/visitors.chatHistory/room/${createdRoom._id}/visitor/${createdVisitor._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.history).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }); + }); + }) + .then(() => done()); }); }); }); @@ -230,37 +251,39 @@ describe('LIVECHAT - visitors', function () { it("should return a 'visitor-has-open-rooms' error when there are open rooms", (done) => { createVisitor().then((createdVisitor: ILivechatVisitor) => { - createLivechatRoom(createdVisitor.token).then(() => { + createLivechatRoom(createdVisitor.token) + .then(() => { + request + .delete(api(`livechat/visitor/${createdVisitor.token}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('Cannot remove visitors with opened rooms [visitor-has-open-rooms]'); + }); + }) + .then(() => done()); + }); + }); + + it('should return a visitor when the query params is all valid', (done) => { + createVisitor() + .then((createdVisitor: ILivechatVisitor) => { request .delete(api(`livechat/visitor/${createdVisitor.token}`)) .set(credentials) .expect('Content-Type', 'application/json') - .expect(400) + .expect(200) .expect((res: Response) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('Cannot remove visitors with opened rooms [visitor-has-open-rooms]'); - }) - .end(done); - }); - }); - }); - - it('should return a visitor when the query params is all valid', (done) => { - createVisitor().then((createdVisitor: ILivechatVisitor) => { - request - .delete(api(`livechat/visitor/${createdVisitor.token}`)) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res: Response) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('visitor'); - expect(res.body.visitor).to.have.property('_id'); - expect(res.body.visitor).to.have.property('ts'); - expect(res.body.visitor._id).to.be.equal(createdVisitor._id); - }) - .end(done); - }); + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('visitor'); + expect(res.body.visitor).to.have.property('_id'); + expect(res.body.visitor).to.have.property('ts'); + expect(res.body.visitor._id).to.be.equal(createdVisitor._id); + }); + }) + .then(() => done()); }); it("should return a 'error-removing-visitor' error when removeGuest's result is false", (done) => { @@ -278,14 +301,11 @@ describe('LIVECHAT - visitors', function () { describe('livechat/visitors.autocomplete', () => { it('should return an error when the user doesnt have the right permissions', (done) => { - updatePermission('view-l-room', []).then(() => - request - .get(api('livechat/visitors.autocomplete')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(403) - .end(done), - ); + updatePermission('view-l-room', []) + .then(() => + request.get(api('livechat/visitors.autocomplete')).set(credentials).expect('Content-Type', 'application/json').expect(403), + ) + .then(() => done()); }); it('should return an error when the "selector" query parameter is not valid', (done) => { @@ -340,9 +360,9 @@ describe('LIVECHAT - visitors', function () { expect(visitor).to.have.property('_id'); expect(visitor).to.have.property('name'); expect(visitor._id).to.be.equal(createdVisitor._id); - }) - .end(done); - }); + }); + }) + .then(() => done()); }); }); @@ -452,6 +472,160 @@ describe('LIVECHAT - visitors', function () { .catch(done); }); }); + + describe('GET [omnichannel/contact.search]', () => { + it('should fail if no email|phone|custom params are passed as query', async () => { + await request.get(api('omnichannel/contact.search')).set(credentials).expect('Content-Type', 'application/json').expect(400); + }); + it('should fail if its trying to find by an empty string', async () => { + await request.get(api('omnichannel/contact.search?email=')).set(credentials).expect('Content-Type', 'application/json').expect(400); + }); + it('should fail if custom is passed but is not JSON serializable', async () => { + await request + .get(api('omnichannel/contact.search?custom={a":1}')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400); + }); + it('should fail if custom is an empty object and no email|phone are provided', async () => { + await request + .get(api('omnichannel/contact.search?custom={}')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400); + }); + it('should find a contact by email', (done) => { + createVisitor() + .then((visitor: ILivechatVisitor) => { + request + .get(api(`omnichannel/contact.search?email=${visitor.visitorEmails?.[0].address}`)) + .set(credentials) + .send() + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.have.property('_id'); + expect(res.body.contact).to.have.property('name'); + expect(res.body.contact).to.have.property('username'); + expect(res.body.contact).to.have.property('phone'); + expect(res.body.contact).to.have.property('visitorEmails'); + expect(res.body.contact._id).to.be.equal(visitor._id); + expect(res.body.contact.phone[0].phoneNumber).to.be.equal(visitor.phone?.[0].phoneNumber); + // done(); + }); + }) + .then(() => done()) + .catch(done); + }); + it('should find a contact by phone', (done) => { + createVisitor() + .then((visitor: ILivechatVisitor) => { + request + .get(api(`omnichannel/contact.search?phone=${visitor.phone?.[0].phoneNumber}`)) + .set(credentials) + .send() + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.have.property('_id'); + expect(res.body.contact).to.have.property('name'); + expect(res.body.contact).to.have.property('username'); + expect(res.body.contact).to.have.property('phone'); + expect(res.body.contact).to.have.property('visitorEmails'); + expect(res.body.contact._id).to.be.equal(visitor._id); + expect(res.body.contact.phone[0].phoneNumber).to.be.equal(visitor.phone?.[0].phoneNumber); + }); + }) + .then(() => done()) + .catch(done); + }); + it('should find a contact by custom field', (done) => { + const cfID = 'address'; + createCustomField({ + searchable: true, + field: 'address', + label: 'address', + defaultValue: 'test_default_address', + scope: 'visitor', + visibility: 'public', + regexp: '', + } as unknown as ILivechatCustomField & { field: string }) + .then((cf) => { + if (!cf) { + throw new Error('Custom field not created'); + } + }) + .then(() => createVisitor()) + .then(() => { + request + .get(api(`omnichannel/contact.search?custom=${JSON.stringify({ address: 'Rocket.Chat' })}`)) + .set(credentials) + .send() + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.contact).to.have.property('name'); + expect(res.body.contact).to.have.property('username'); + expect(res.body.contact).to.have.property('phone'); + expect(res.body.contact).to.have.property('visitorEmails'); + expect(res.body.contact.livechatData).to.have.property('address', 'Rocket.Chat street'); + deleteCustomField(cfID); + }); + }) + .then(() => done()) + .catch(done); + }); + + it('should return null if an invalid set of custom fields is passed and no other params are sent', async () => { + const res = await request + .get(api(`omnichannel/contact.search?custom=${JSON.stringify({ nope: 'nel' })}`)) + .set(credentials) + .send(); + expect(res.body).to.have.property('success', true); + expect(res.body.contact).to.be.null; + }); + + it('should not break if more than 1 custom field are passed', async () => { + const res = await request + .get(api(`omnichannel/contact.search?custom=${JSON.stringify({ nope: 'nel', another: 'field' })}`)) + .set(credentials) + .send(); + expect(res.body).to.have.property('success', true); + expect(res.body.contact).to.be.null; + }); + + it('should not break if bad things are passed as custom field keys', async () => { + const res = await request + .get(api(`omnichannel/contact.search?custom=${JSON.stringify({ $regex: 'nel' })}`)) + .set(credentials) + .send(); + expect(res.body).to.have.property('success', true); + expect(res.body.contact).to.be.null; + }); + + it('should not break if bad things are passed as custom field keys 2', async () => { + const res = await request + .get(api(`omnichannel/contact.search?custom=${JSON.stringify({ '$regex: { very-bad }': 'nel' })}`)) + .set(credentials) + .send(); + expect(res.body).to.have.property('success', true); + expect(res.body.contact).to.be.null; + }); + + it('should not break if bad things are passed as custom field values', async () => { + const res = await request + .get(api(`omnichannel/contact.search?custom=${JSON.stringify({ nope: '^((ab)*)+$' })}`)) + .set(credentials) + .send(); + expect(res.body).to.have.property('success', true); + expect(res.body.contact).to.be.null; + }); + }); }); // TODO: Missing tests for the following endpoints: diff --git a/packages/core-typings/src/ILivechatCustomField.ts b/packages/core-typings/src/ILivechatCustomField.ts index 9f1ce0ffad5f..e4e66d545da6 100644 --- a/packages/core-typings/src/ILivechatCustomField.ts +++ b/packages/core-typings/src/ILivechatCustomField.ts @@ -10,4 +10,5 @@ export interface ILivechatCustomField extends IRocketChatRecord { defaultValue?: string; options?: string; public?: boolean; + searchable?: boolean; } diff --git a/packages/model-typings/src/models/ILivechatCustomFieldModel.ts b/packages/model-typings/src/models/ILivechatCustomFieldModel.ts index 68ead81af0bf..ebe7f420868a 100644 --- a/packages/model-typings/src/models/ILivechatCustomFieldModel.ts +++ b/packages/model-typings/src/models/ILivechatCustomFieldModel.ts @@ -7,4 +7,15 @@ import type { IBaseModel } from './IBaseModel'; export interface ILivechatCustomFieldModel extends IBaseModel { findByScope(scope: ILivechatCustomField['scope'], options?: FindOptions): FindCursor; findByScope(scope: ILivechatCustomField['scope'], options?: FindOptions): FindCursor; + findMatchingCustomFields( + scope: ILivechatCustomField['scope'], + searchable: boolean, + options?: FindOptions, + ): FindCursor; + findMatchingCustomFieldsByIds( + ids: ILivechatCustomField['_id'][], + scope: ILivechatCustomField['scope'], + searchable: boolean, + options?: FindOptions, + ): FindCursor; } diff --git a/packages/model-typings/src/models/ILivechatVisitorsModel.ts b/packages/model-typings/src/models/ILivechatVisitorsModel.ts index e50432c76879..4e381ce55664 100644 --- a/packages/model-typings/src/models/ILivechatVisitorsModel.ts +++ b/packages/model-typings/src/models/ILivechatVisitorsModel.ts @@ -17,11 +17,18 @@ export interface ILivechatVisitorsModel extends IBaseModel { custom_name: string; } >; - findPaginatedVisitorsByEmailOrPhoneOrNameOrUsername( + findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField( emailOrPhone: string, nameOrUsername: RegExp, - options: FindOptions, - ): FindPaginated>; + allowedCustomFields: string[], + options?: FindOptions, + ): Promise>>; + + findOneByEmailAndPhoneAndCustomField( + email: string | null | undefined, + phone: string | null | undefined, + customFields?: { [key: string]: RegExp }, + ): Promise; removeContactManagerByUsername(manager: string): Promise;