diff --git a/apps/meteor/app/livechat/imports/server/rest/appearance.ts b/apps/meteor/app/livechat/imports/server/rest/appearance.ts index f688b9da7f8..0fa365be187 100644 --- a/apps/meteor/app/livechat/imports/server/rest/appearance.ts +++ b/apps/meteor/app/livechat/imports/server/rest/appearance.ts @@ -1,9 +1,18 @@ +import { Settings } from '@rocket.chat/models'; +import { isPOSTLivechatAppearanceParams } from '@rocket.chat/rest-typings'; + import { API } from '../../../../api/server'; import { findAppearance } from '../../../server/api/lib/appearance'; API.v1.addRoute( 'livechat/appearance', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: { + POST: isPOSTLivechatAppearanceParams, + }, + }, { async get() { const { appearance } = await findAppearance(); @@ -12,5 +21,44 @@ API.v1.addRoute( appearance, }); }, + async post() { + const settings = this.bodyParams; + + const validSettingList = [ + 'Livechat_title', + 'Livechat_title_color', + 'Livechat_enable_message_character_limit', + 'Livechat_message_character_limit', + 'Livechat_show_agent_info', + 'Livechat_show_agent_email', + 'Livechat_display_offline_form', + 'Livechat_offline_form_unavailable', + 'Livechat_offline_message', + 'Livechat_offline_success_message', + 'Livechat_offline_title', + 'Livechat_offline_title_color', + 'Livechat_offline_email', + 'Livechat_conversation_finished_message', + 'Livechat_conversation_finished_text', + 'Livechat_registration_form', + 'Livechat_name_field_registration_form', + 'Livechat_email_field_registration_form', + 'Livechat_registration_form_message', + ]; + + const valid = settings.every((setting) => validSettingList.includes(setting._id)); + + if (!valid) { + throw new Error('invalid-setting'); + } + + await Promise.all( + settings.map((setting) => { + return Settings.updateValueById(setting._id, setting.value); + }), + ); + + return API.v1.success(); + }, }, ); diff --git a/apps/meteor/app/livechat/imports/server/rest/triggers.ts b/apps/meteor/app/livechat/imports/server/rest/triggers.ts index fa3d0dada05..a12d4a98828 100644 --- a/apps/meteor/app/livechat/imports/server/rest/triggers.ts +++ b/apps/meteor/app/livechat/imports/server/rest/triggers.ts @@ -1,4 +1,5 @@ -import { isGETLivechatTriggersParams } from '@rocket.chat/rest-typings'; +import { LivechatTrigger } from '@rocket.chat/models'; +import { isGETLivechatTriggersParams, isPOSTLivechatTriggersParams } from '@rocket.chat/rest-typings'; import { API } from '../../../../api/server'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; @@ -6,7 +7,14 @@ import { findTriggers, findTriggerById } from '../../../server/api/lib/triggers' API.v1.addRoute( 'livechat/triggers', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETLivechatTriggersParams }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: { + GET: isGETLivechatTriggersParams, + POST: isPOSTLivechatTriggersParams, + }, + }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); @@ -22,6 +30,17 @@ API.v1.addRoute( return API.v1.success(triggers); }, + async post() { + const { _id, name, description, enabled, runOnce, conditions, actions } = this.bodyParams; + + if (_id) { + await LivechatTrigger.updateById(_id, { name, description, enabled, runOnce, conditions, actions }); + } else { + await LivechatTrigger.insertOne({ name, description, enabled, runOnce, conditions, actions }); + } + + return API.v1.success(); + }, }, ); diff --git a/apps/meteor/app/livechat/server/api/v1/transcript.ts b/apps/meteor/app/livechat/server/api/v1/transcript.ts index 29d1ceeb106..3eaa91c37c7 100644 --- a/apps/meteor/app/livechat/server/api/v1/transcript.ts +++ b/apps/meteor/app/livechat/server/api/v1/transcript.ts @@ -1,7 +1,10 @@ -import { isPOSTLivechatTranscriptParams } from '@rocket.chat/rest-typings'; +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatRooms, Users } from '@rocket.chat/models'; +import { isPOSTLivechatTranscriptParams, isPOSTLivechatTranscriptRequestParams } from '@rocket.chat/rest-typings'; import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; +import { Livechat as LivechatJS } from '../../lib/Livechat'; import { Livechat } from '../../lib/LivechatTyped'; API.v1.addRoute( @@ -18,3 +21,45 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'livechat/transcript/:rid', + { + authRequired: true, + permissionsRequired: ['send-omnichannel-chat-transcript'], + validateParams: { + POST: isPOSTLivechatTranscriptRequestParams, + }, + }, + { + async delete() { + const { rid } = this.urlParams; + const room = await LivechatRooms.findOneById>(rid, { + projection: { open: 1, transcriptRequest: 1 }, + }); + + if (!room?.open) { + throw new Error('error-invalid-room'); + } + if (!room.transcriptRequest) { + throw new Error('error-transcript-not-requested'); + } + + await LivechatRooms.unsetEmailTranscriptRequestedByRoomId(rid); + + return API.v1.success(); + }, + async post() { + const { rid } = this.urlParams; + const { email, subject } = this.bodyParams; + + const user = await Users.findOneById(this.userId, { + projection: { _id: 1, username: 1, name: 1, utcOffset: 1 }, + }); + + await LivechatJS.requestTranscript({ rid, email, subject, user }); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/methods/discardTranscript.ts b/apps/meteor/app/livechat/server/methods/discardTranscript.ts index fec443444d1..2e03b6ebd92 100644 --- a/apps/meteor/app/livechat/server/methods/discardTranscript.ts +++ b/apps/meteor/app/livechat/server/methods/discardTranscript.ts @@ -4,6 +4,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -14,6 +15,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:discardTranscript'(rid: string) { + methodDeprecationLogger.method('livechat:discardTranscript', '7.0.0'); check(rid, String); const user = Meteor.userId(); diff --git a/apps/meteor/app/livechat/server/methods/requestTranscript.ts b/apps/meteor/app/livechat/server/methods/requestTranscript.ts index e114daa0aa9..02ac65a4817 100644 --- a/apps/meteor/app/livechat/server/methods/requestTranscript.ts +++ b/apps/meteor/app/livechat/server/methods/requestTranscript.ts @@ -4,6 +4,7 @@ import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { Livechat } from '../lib/Livechat'; declare module '@rocket.chat/ui-contexts' { @@ -15,6 +16,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:requestTranscript'(rid, email, subject) { + methodDeprecationLogger.method('livechat:requestTranscript', '7.0.0'); check(rid, String); check(email, String); diff --git a/apps/meteor/app/livechat/server/methods/saveAppearance.ts b/apps/meteor/app/livechat/server/methods/saveAppearance.ts index 3bf9ad7070a..35152d136af 100644 --- a/apps/meteor/app/livechat/server/methods/saveAppearance.ts +++ b/apps/meteor/app/livechat/server/methods/saveAppearance.ts @@ -3,6 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -13,6 +14,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:saveAppearance'(settings) { + methodDeprecationLogger.method('livechat:saveAppearance', '7.0.0'); const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { diff --git a/apps/meteor/app/livechat/server/methods/saveTrigger.ts b/apps/meteor/app/livechat/server/methods/saveTrigger.ts index 18b17f68746..37f78d08120 100644 --- a/apps/meteor/app/livechat/server/methods/saveTrigger.ts +++ b/apps/meteor/app/livechat/server/methods/saveTrigger.ts @@ -5,6 +5,7 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -15,6 +16,7 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ async 'livechat:saveTrigger'(trigger) { + methodDeprecationLogger.method('livechat:saveTrigger', '7.0.0'); const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { diff --git a/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx b/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx index 3f1c7a9ad4a..4bdbf94ff2f 100644 --- a/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx +++ b/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx @@ -1,7 +1,7 @@ import type { ISetting, Serialized } from '@rocket.chat/core-typings'; import { ButtonGroup, Button, Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import type { FC } from 'react'; import React from 'react'; @@ -47,12 +47,15 @@ const AppearancePage: FC = ({ settings }) => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const save = useMethod('livechat:saveAppearance'); + const save = useEndpoint('POST', '/v1/livechat/appearance'); const { values, handlers, commit, reset, hasUnsavedChanges } = useForm(reduceAppearance(settings)); const handleSave = useMutableCallback(async () => { - const mappedAppearance = Object.entries(values).map(([_id, value]) => ({ _id, value })); + const mappedAppearance = Object.entries(values).map(([_id, value]) => ({ _id, value })) as { + _id: string; + value: string | boolean | number; + }[]; try { await save(mappedAppearance); diff --git a/apps/meteor/client/views/omnichannel/triggers/EditTriggerPage.js b/apps/meteor/client/views/omnichannel/triggers/EditTriggerPage.js index 17f9bb2b744..cc2edf7b919 100644 --- a/apps/meteor/client/views/omnichannel/triggers/EditTriggerPage.js +++ b/apps/meteor/client/views/omnichannel/triggers/EditTriggerPage.js @@ -1,6 +1,6 @@ import { FieldGroup, Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar'; @@ -44,7 +44,7 @@ const EditTriggerPage = ({ data, onSave }) => { const router = useRoute('omnichannel-triggers'); - const save = useMethod('livechat:saveTrigger'); + const save = useEndpoint('POST', '/v1/livechat/triggers'); const { values, handlers, hasUnsavedChanges } = useForm(getInitialValues(data)); diff --git a/apps/meteor/client/views/omnichannel/triggers/NewTriggerPage.js b/apps/meteor/client/views/omnichannel/triggers/NewTriggerPage.js index a9b0b4f780f..9f696ebe1aa 100644 --- a/apps/meteor/client/views/omnichannel/triggers/NewTriggerPage.js +++ b/apps/meteor/client/views/omnichannel/triggers/NewTriggerPage.js @@ -1,6 +1,6 @@ import { Button, FieldGroup, ButtonGroup } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar'; @@ -13,7 +13,7 @@ const NewTriggerPage = ({ onSave }) => { const router = useRoute('omnichannel-triggers'); - const save = useMethod('livechat:saveTrigger'); + const save = useEndpoint('POST', '/v1/livechat/triggers'); const { values, handlers } = useForm({ name: '', diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx index 880a4bea5c8..7f376341992 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -72,12 +72,12 @@ export const useQuickActions = (): { const closeModal = useCallback(() => setModal(null), [setModal]); - const requestTranscript = useMethod('livechat:requestTranscript'); + const requestTranscript = useEndpoint('POST', '/v1/livechat/transcript/:rid', { rid }); const handleRequestTranscript = useCallback( async (email: string, subject: string) => { try { - await requestTranscript(rid, email, subject); + await requestTranscript({ email, subject }); closeModal(); dispatchToastMessage({ type: 'success', @@ -87,7 +87,7 @@ export const useQuickActions = (): { dispatchToastMessage({ type: 'error', message: error }); } }, - [closeModal, dispatchToastMessage, requestTranscript, rid, t], + [closeModal, dispatchToastMessage, requestTranscript, t], ); const sendTranscriptPDF = useEndpoint('POST', '/v1/omnichannel/:rid/request-transcript', { rid }); @@ -118,11 +118,11 @@ export const useQuickActions = (): { [closeModal, dispatchToastMessage, rid, sendTranscript], ); - const discardTranscript = useMethod('livechat:discardTranscript'); + const discardTranscript = useEndpoint('DELETE', '/v1/livechat/transcript/:rid', { rid }); const handleDiscardTranscript = useCallback(async () => { try { - await discardTranscript(rid); + await discardTranscript(); dispatchToastMessage({ type: 'success', message: t('Livechat_transcript_request_has_been_canceled'), @@ -131,7 +131,7 @@ export const useQuickActions = (): { } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } - }, [closeModal, discardTranscript, dispatchToastMessage, rid, t]); + }, [closeModal, discardTranscript, dispatchToastMessage, t]); const forwardChat = useEndpoint('POST', '/v1/livechat/room.forward'); diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index e55f712ab6b..d7bc9a1633b 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -180,7 +180,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive }; const params = [...firstParams, usersGroup, project, facet]; - return this.col.aggregate(params, { readPreference: readSecondaryPreferred() }).toArray(); + return this.col.aggregate(params, { readPreference: readSecondaryPreferred(), allowDiskUse: true }).toArray(); } async findAllNumberOfAbandonedRooms({ diff --git a/apps/meteor/server/models/raw/LivechatTrigger.ts b/apps/meteor/server/models/raw/LivechatTrigger.ts index de719769b2b..6e5db23d5ac 100644 --- a/apps/meteor/server/models/raw/LivechatTrigger.ts +++ b/apps/meteor/server/models/raw/LivechatTrigger.ts @@ -17,7 +17,7 @@ export class LivechatTriggerRaw extends BaseRaw implements ILi return this.find({ enabled: true }); } - updateById(_id: string, data: ILivechatTrigger): Promise { + updateById(_id: string, data: Omit): Promise { return this.updateOne({ _id }, { $set: data } as UpdateFilter); // TODO: remove this cast when TypeScript is updated } } diff --git a/apps/meteor/tests/end-to-end/api/livechat/02-appearance.ts b/apps/meteor/tests/end-to-end/api/livechat/02-appearance.ts index 17e2917f4e1..6df7919bea0 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/02-appearance.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/02-appearance.ts @@ -3,7 +3,7 @@ import { before, describe, it } from 'mocha'; import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; -import { updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper'; describe('LIVECHAT - appearance', function () { this.retries(0); @@ -32,4 +32,53 @@ describe('LIVECHAT - appearance', function () { }); }); }); + + describe('POST livechat/appearance', () => { + it('should fail if user is not logged in', async () => { + await request.post(api('livechat/appearance')).send({}).expect(401); + }); + it('should fail if body is not an array', async () => { + await request.post(api('livechat/appearance')).set(credentials).send({}).expect(400); + }); + it('should fail if body is an empty array', async () => { + await request.post(api('livechat/appearance')).set(credentials).send([]).expect(400); + }); + it('should fail if body does not contain value', async () => { + await request + .post(api('livechat/appearance')) + .set(credentials) + .send([{ name: 'Livechat_title' }]) + .expect(400); + }); + it('should fail if body does not contain name', async () => { + await request + .post(api('livechat/appearance')) + .set(credentials) + .send([{ value: 'test' }]) + .expect(400); + }); + it('should fail if user does not have the necessary permission', async () => { + await removePermissionFromAllRoles('view-livechat-manager'); + await request + .post(api('livechat/appearance')) + .set(credentials) + .send([{ _id: 'invalid', value: 'test' }]) + .expect(403); + }); + it('should fail if body contains invalid _id', async () => { + await restorePermissionToRoles('view-livechat-manager'); + await request + .post(api('livechat/appearance')) + .set(credentials) + .send([{ _id: 'invalid', value: 'test' }]) + .expect(400); + }); + it('should update the settings', async () => { + await request + .post(api('livechat/appearance')) + .set(credentials) + .send([{ _id: 'Livechat_title', value: 'test' }]) + .expect(200); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts b/apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts index b6ae9cf84ac..9f467b4eeaa 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts @@ -4,7 +4,7 @@ import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { createTrigger, fetchTriggers } from '../../../data/livechat/triggers'; -import { updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper'; describe('LIVECHAT - triggers', function () { this.retries(0); @@ -17,12 +17,12 @@ describe('LIVECHAT - triggers', function () { describe('livechat/triggers', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-manager', []); + await removePermissionFromAllRoles('view-livechat-manager'); await request.get(api('livechat/triggers')).set(credentials).expect('Content-Type', 'application/json').expect(403); }); it('should return an array of triggers', async () => { - await updatePermission('view-livechat-manager', ['admin']); + await restorePermissionToRoles('view-livechat-manager'); await createTrigger(`test${Date.now()}`); await request .get(api('livechat/triggers')) @@ -46,6 +46,167 @@ describe('LIVECHAT - triggers', function () { }); }); + describe('POST livechat/triggers', () => { + it('should fail if user is not logged in', async () => { + await request.post(api('livechat/triggers')).send({}).expect(401); + }); + it('should fail if no data is sent', async () => { + await request.post(api('livechat/triggers')).set(credentials).send({}).expect(400); + }); + it('should fail if invalid data is sent', async () => { + await request.post(api('livechat/triggers')).set(credentials).send({ name: 'test' }).expect(400); + }); + it('should fail if name is not an string', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ name: 1, description: 'test', enabled: true, runOnce: true, conditions: [], actions: [] }) + .expect(400); + }); + it('should fail if description is not an string', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ name: 'test', description: 1, enabled: true, runOnce: true, conditions: [], actions: [] }) + .expect(400); + }); + it('should fail if enabled is not an boolean', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ name: 'test', description: 'test', enabled: 1, runOnce: true, conditions: [], actions: [] }) + .expect(400); + }); + it('should fail if runOnce is not an boolean', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ name: 'test', description: 'test', enabled: true, runOnce: 1, conditions: [], actions: [] }) + .expect(400); + }); + it('should fail if conditions is not an array', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: 1, actions: [] }) + .expect(400); + }); + it('should fail if actions is not an array', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: [], actions: 1 }) + .expect(400); + }); + it('should fail if conditions is an array with invalid data', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: [1], actions: [] }) + .expect(400); + }); + it('should fail if conditions is an array of objects, but name is not a valid value', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: [{ name: 'invalid' }], actions: [] }) + .expect(400); + }); + it('should fail if actions is an array of invalid values', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: [{ name: 'page-url' }], actions: [1] }) + .expect(400); + }); + it('should fail if actions is an array of objects, but name is not a valid value', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ + name: 'test', + description: 'test', + enabled: true, + runOnce: true, + conditions: [{ name: 'page-url', value: 'http://localhost:3000' }], + actions: [{ name: 'invalid' }], + }) + .expect(400); + }); + it('should fail if actions is an array of objects, but sender is not a valid value', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ + name: 'test', + description: 'test', + enabled: true, + runOnce: true, + conditions: [{ name: 'page-url' }], + actions: [{ name: 'send-message', params: { sender: 'invalid' } }], + }) + .expect(400); + }); + it('should fail if actions is an array of objects, but msg is not a valid value', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ + name: 'test', + description: 'test', + enabled: true, + runOnce: true, + conditions: [{ name: 'page-url', value: 'http://localhost:3000' }], + actions: [{ name: 'send-message', params: { sender: 'custom' } }], + }) + .expect(400); + }); + it('should fail if actions is an array of objects, but name is not a valid value', async () => { + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ + name: 'test', + description: 'test', + enabled: true, + runOnce: true, + conditions: [{ name: 'page-url', value: 'http://localhost:3000' }], + actions: [{ name: 'send-message', params: { sender: 'custom', msg: 'test', name: {} } }], + }) + .expect(400); + }); + it('should fail if user doesnt have view-livechat-manager permission', async () => { + await removePermissionFromAllRoles('view-livechat-manager'); + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ + name: 'test', + description: 'test', + enabled: true, + runOnce: true, + conditions: [{ name: 'page-url', value: 'http://localhost:3000' }], + actions: [{ name: 'send-message', params: { sender: 'custom', msg: 'test', name: 'test' } }], + }) + .expect(403); + }); + it('should save a new trigger', async () => { + await restorePermissionToRoles('view-livechat-manager'); + await request + .post(api('livechat/triggers')) + .set(credentials) + .send({ + name: 'test', + description: 'test', + enabled: true, + runOnce: true, + conditions: [{ name: 'page-url', value: 'http://localhost:3000' }], + actions: [{ name: 'send-message', params: { sender: 'custom', msg: 'test', name: 'test' } }], + }) + .expect(200); + }); + }); + describe('livechat/triggers/:id', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { await updatePermission('view-livechat-manager', []); diff --git a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts index 09f5cbb490e..694c8117fbb 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts @@ -4,9 +4,9 @@ import { before, describe, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields'; import { addOrRemoveAgentFromDepartment, createDepartmentWithAnOnlineAgent } from '../../../data/livechat/department'; -import { createVisitor, createLivechatRoom, makeAgentUnavailable } from '../../../data/livechat/rooms'; +import { createVisitor, createLivechatRoom, makeAgentUnavailable, closeOmnichannelRoom } from '../../../data/livechat/rooms'; import { createBotAgent, getRandomVisitorToken } from '../../../data/livechat/users'; -import { updateSetting } from '../../../data/permissions.helper'; +import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper'; import { IS_EE } from '../../../e2e/config/constants'; describe('LIVECHAT - Utils', function () { @@ -276,6 +276,140 @@ describe('LIVECHAT - Utils', function () { expect(body).to.have.property('success', true); }); }); + describe('livechat/transcript/:rid', () => { + it('should fail if user is not authenticated', async () => { + await request.delete(api('livechat/transcript/rid')).send({}).expect(401); + }); + it('should fail if user doesnt have "send-omnichannel-chat-transcript" permission', async () => { + const user = await createVisitor(); + const room = await createLivechatRoom(user.token); + await removePermissionFromAllRoles('send-omnichannel-chat-transcript'); + + await request + .delete(api(`livechat/transcript/${room._id}`)) + .set(credentials) + .send({}) + .expect(403); + }); + it('should fail if rid is not a valid room id', async () => { + await restorePermissionToRoles('send-omnichannel-chat-transcript'); + await request.delete(api('livechat/transcript/rid')).set(credentials).send({}).expect(400); + }); + it('should fail if room is not open', async () => { + const user = await createVisitor(); + const room = await createLivechatRoom(user.token); + await closeOmnichannelRoom(room._id); + await request + .delete(api(`livechat/transcript/${room._id}`)) + .set(credentials) + .send({}) + .expect(400); + }); + it('should fail if room doesnt have transcript requested', async () => { + const user = await createVisitor(); + const room = await createLivechatRoom(user.token); + + await request + .delete(api(`livechat/transcript/${room._id}`)) + .set(credentials) + .send({}) + .expect(400); + }); + it('should remove transcript if all good', async () => { + const user = await createVisitor(); + const room = await createLivechatRoom(user.token); + + await request + .post(api(`livechat/transcript/${room._id}`)) + .set(credentials) + .send({ email: 'abc@abc.com', subject: 'test' }) + .expect(200); + + await request + .delete(api(`livechat/transcript/${room._id}`)) + .set(credentials) + .send({}) + .expect(200); + }); + }); + + describe('POST livechat/transcript/:rid', () => { + it('should fail if user is not authenticated', async () => { + await request.post(api('livechat/transcript/rid')).send({}).expect(401); + }); + it('should fail if "email" param is not sent', async () => { + const user = await createVisitor(); + const room = await createLivechatRoom(user.token); + + await request + .post(api(`livechat/transcript/${room._id}`)) + .set(credentials) + .send({}) + .expect(400); + }); + it('should fail if "subject" param is not sent', async () => { + const user = await createVisitor(); + const room = await createLivechatRoom(user.token); + + await request + .post(api(`livechat/transcript/${room._id}`)) + .set(credentials) + .send({ email: 'abc@abc.xmz' }) + .expect(400); + }); + it('should fail if user doesnt have "send-omnichannel-chat-transcript" permission', async () => { + const user = await createVisitor(); + const room = await createLivechatRoom(user.token); + await updatePermission('send-omnichannel-chat-transcript', []); + + await request + .post(api(`livechat/transcript/${room._id}`)) + .set(credentials) + .send({ email: 'abc@abc.com', subject: 'test' }) + .expect(403); + }); + it('should fail if rid is not a valid room id', async () => { + await updatePermission('send-omnichannel-chat-transcript', ['livechat-manager', 'admin']); + await request.post(api('livechat/transcript/rid')).set(credentials).send({ email: 'abc@abc.com', subject: 'test' }).expect(400); + }); + it('should fail if room is not open', async () => { + const user = await createVisitor(); + const room = await createLivechatRoom(user.token); + await closeOmnichannelRoom(room._id); + await request + .post(api(`livechat/transcript/${room._id}`)) + .set(credentials) + .send({ email: 'abc@abc.com', subject: 'test' }) + .expect(400); + }); + it('should fail if room already has transcript requested', async () => { + const user = await createVisitor(); + const room = await createLivechatRoom(user.token); + + await request + .post(api(`livechat/transcript/${room._id}`)) + .set(credentials) + .send({ email: 'abc@abc.com', subject: 'test' }) + .expect(200); + + await request + .post(api(`livechat/transcript/${room._id}`)) + .set(credentials) + .send({ email: 'abc@abc.com', subject: 'test' }) + .expect(400); + }); + it('should request transcript if all good', async () => { + const user = await createVisitor(); + const room = await createLivechatRoom(user.token); + + await request + .post(api(`livechat/transcript/${room._id}`)) + .set(credentials) + .send({ email: 'abc@abc.com', subject: 'test' }) + .expect(200); + }); + }); + describe('livechat/visitor.callStatus', () => { it('should fail if token is not in body params', async () => { const { body } = await request.post(api('livechat/visitor.callStatus')).set(credentials).send({}); diff --git a/packages/model-typings/src/models/ILivechatTriggerModel.ts b/packages/model-typings/src/models/ILivechatTriggerModel.ts index 85d3695ed89..926b1c5b77e 100644 --- a/packages/model-typings/src/models/ILivechatTriggerModel.ts +++ b/packages/model-typings/src/models/ILivechatTriggerModel.ts @@ -5,5 +5,5 @@ import type { IBaseModel } from './IBaseModel'; export interface ILivechatTriggerModel extends IBaseModel { findEnabled(): FindCursor; - updateById(_id: string, data: ILivechatTrigger): Promise; + updateById(_id: string, data: Omit): Promise; } diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index dcc064f6b79..1ee29ea555c 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -21,6 +21,8 @@ import type { IOmnichannelServiceLevelAgreements, ILivechatPriority, LivechatDepartmentDTO, + ILivechatTriggerCondition, + ILivechatTriggerAction, } from '@rocket.chat/core-typings'; import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; @@ -2982,11 +2984,149 @@ const POSTomnichannelIntegrationsSchema = { export const isPOSTomnichannelIntegrations = ajv.compile(POSTomnichannelIntegrationsSchema); +type POSTLivechatTranscriptRequestParams = { + email: string; + subject: string; +}; + +const POSTLivechatTranscriptRequestParamsSchema = { + type: 'object', + properties: { + email: { + type: 'string', + }, + subject: { + type: 'string', + }, + }, + required: ['email', 'subject'], + additionalProperties: false, +}; + +export const isPOSTLivechatTranscriptRequestParams = ajv.compile( + POSTLivechatTranscriptRequestParamsSchema, +); + +type POSTLivechatTriggersParams = { + name: string; + description: string; + enabled: boolean; + runOnce: boolean; + conditions: ILivechatTriggerCondition[]; + actions: ILivechatTriggerAction[]; + _id?: string; +}; + +const POSTLivechatTriggersParamsSchema = { + type: 'object', + properties: { + name: { + type: 'string', + }, + description: { + type: 'string', + }, + enabled: { + type: 'boolean', + }, + runOnce: { + type: 'boolean', + }, + conditions: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + enum: ['time-on-site', 'page-url', 'chat-opened-by-visitor'], + }, + value: { + type: 'string', + nullable: true, + }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + minItems: 1, + }, + actions: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + enum: ['send-message'], + }, + params: { + type: 'object', + nullable: true, + properties: { + sender: { + type: 'string', + enum: ['queue', 'custom'], + }, + msg: { + type: 'string', + }, + name: { + type: 'string', + nullable: true, + }, + }, + required: ['sender', 'msg'], + additionalProperties: false, + }, + }, + required: ['name'], + additionalProperties: false, + }, + minItems: 1, + }, + _id: { + type: 'string', + nullable: true, + }, + }, + required: ['name', 'description', 'enabled', 'runOnce', 'conditions', 'actions'], + additionalProperties: false, +}; + +export const isPOSTLivechatTriggersParams = ajv.compile(POSTLivechatTriggersParamsSchema); + +type POSTLivechatAppearanceParams = { + _id: string; + value: string | boolean | number; +}[]; + +const POSTLivechatAppearanceParamsSchema = { + type: 'array', + items: { + type: 'object', + properties: { + _id: { + type: 'string', + }, + value: { + type: ['string', 'boolean', 'number'], + }, + }, + required: ['_id', 'value'], + additionalProperties: false, + }, + minItems: 1, +}; + +export const isPOSTLivechatAppearanceParams = ajv.compile(POSTLivechatAppearanceParamsSchema); + export type OmnichannelEndpoints = { '/v1/livechat/appearance': { GET: () => { appearance: ISetting[]; }; + POST: (params: POSTLivechatAppearanceParams) => void; }; '/v1/livechat/visitors.info': { GET: (params: LivechatVisitorsInfo) => { @@ -3287,6 +3427,10 @@ export type OmnichannelEndpoints = { '/v1/livechat/transcript': { POST: (params: POSTLivechatTranscriptParams) => { message: string }; }; + '/v1/livechat/transcript/:rid': { + DELETE: () => void; + POST: (params: POSTLivechatTranscriptRequestParams) => void; + }; '/v1/livechat/offline.message': { POST: (params: POSTLivechatOfflineMessageParams) => { message: string }; }; @@ -3351,6 +3495,7 @@ export type OmnichannelEndpoints = { }; '/v1/livechat/triggers': { GET: (params: GETLivechatTriggersParams) => PaginatedResult<{ triggers: WithId[] }>; + POST: (params: POSTLivechatTriggersParams) => void; }; '/v1/livechat/triggers/:_id': { GET: () => { trigger: ILivechatTrigger | null };