diff --git a/.changeset/calm-hounds-look.md b/.changeset/calm-hounds-look.md new file mode 100644 index 0000000000000..5bbc19ad8667a --- /dev/null +++ b/.changeset/calm-hounds-look.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Adds deprecation warning on `livechat:returnAsInquiry` with new endpoint replacing it; `livechat/inquiries.returnAsInquiry` diff --git a/apps/meteor/app/livechat/imports/server/rest/inquiries.ts b/apps/meteor/app/livechat/imports/server/rest/inquiries.ts index 13ecab3df9037..a69604bd083ec 100644 --- a/apps/meteor/app/livechat/imports/server/rest/inquiries.ts +++ b/apps/meteor/app/livechat/imports/server/rest/inquiries.ts @@ -1,15 +1,22 @@ import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; -import { LivechatInquiry, LivechatDepartment, Users } from '@rocket.chat/models'; +import { LivechatInquiry, LivechatDepartment, Users, LivechatRooms } from '@rocket.chat/models'; import { isGETLivechatInquiriesListParams, isPOSTLivechatInquiriesTakeParams, isGETLivechatInquiriesQueuedForUserParams, isGETLivechatInquiriesGetOneParams, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, + validateForbiddenErrorResponse, + isPOSTLivechatInquiriesReturnAsInquiry, + POSTLivechatInquiriesReturnAsInquirySuccessResponse, } from '@rocket.chat/rest-typings'; import { API } from '../../../../api/server'; +import type { ExtractRoutesFromAPI } from '../../../../api/server/ApiClass'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; import { findInquiries, findOneInquiryByRoomId } from '../../../server/api/lib/inquiries'; +import { returnRoomAsInquiry } from '../../../server/lib/rooms'; import { takeInquiry } from '../../../server/lib/takeInquiry'; API.v1.addRoute( @@ -108,3 +115,45 @@ API.v1.addRoute( }, }, ); + +const livechatInquiriesEndpoints = API.v1.post( + 'livechat/inquiries.returnAsInquiry', + { + response: { + 200: POSTLivechatInquiriesReturnAsInquirySuccessResponse, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, + authRequired: true, + permissionsRequired: ['view-l-room'], + body: isPOSTLivechatInquiriesReturnAsInquiry, + }, + async function action() { + const { roomId, departmentId } = this.bodyParams; + + try { + const room = await LivechatRooms.findOneById(roomId); + if (!room) { + return API.v1.failure('error-room-not-found'); + } + + const result = await returnRoomAsInquiry(room, departmentId); + + return API.v1.success({ result }); + } catch (error) { + if (error instanceof Meteor.Error && typeof error.error === 'string') { + return API.v1.failure(error.error as string); + } + + return API.v1.failure('error-returning-inquiry'); + } + }, +); + +type LivechatInquiriesEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends LivechatInquiriesEndpoints {} +} diff --git a/apps/meteor/app/livechat/server/lib/rooms.ts b/apps/meteor/app/livechat/server/lib/rooms.ts index b52dc76b25f64..00ede94ce8a93 100644 --- a/apps/meteor/app/livechat/server/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/lib/rooms.ts @@ -1,4 +1,5 @@ import { AppEvents, Apps } from '@rocket.chat/apps'; +import { Omnichannel } from '@rocket.chat/core-services'; import type { ILivechatVisitor, IMessage, @@ -211,13 +212,17 @@ export async function saveRoomInfo( export async function returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: string, overrideTransferData: Partial = {}) { livechatLogger.debug({ msg: `Transfering room to ${departmentId ? 'department' : ''} queue`, room }); if (!room.open) { - throw new Meteor.Error('room-closed'); + throw new Meteor.Error('room-closed', 'Room closed'); } if (room.onHold) { throw new Meteor.Error('error-room-onHold'); } + if (!(await Omnichannel.isWithinMACLimit(room))) { + throw new Meteor.Error('error-mac-limit-reached'); + } + if (!room.servedBy) { return false; } diff --git a/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts b/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts index df4f4f2a5f76b..40a1714955e49 100644 --- a/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts +++ b/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts @@ -1,10 +1,10 @@ -import { Omnichannel } from '@rocket.chat/core-services'; import type { ILivechatDepartment, IRoom } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { LivechatRooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { returnRoomAsInquiry } from '../lib/rooms'; declare module '@rocket.chat/ddp-client' { @@ -16,6 +16,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async 'livechat:returnAsInquiry'(rid, departmentId) { + methodDeprecationLogger.method('livechat:returnAsInquiry', '8.0.0', '/v1/livechat/inquiries.returnAsInquiry'); const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'view-l-room'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { @@ -30,14 +31,6 @@ Meteor.methods({ }); } - if (!(await Omnichannel.isWithinMACLimit(room))) { - throw new Meteor.Error('error-mac-limit-reached', 'MAC limit reached', { method: 'livechat:returnAsInquiry' }); - } - - if (!room.open) { - throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:returnAsInquiry' }); - } - return returnRoomAsInquiry(room, departmentId); }, }); diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts index 00d990d97cc19..73d9379dd6534 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts @@ -1,5 +1,5 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { useMethod } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query'; @@ -8,13 +8,13 @@ import { roomsQueryKeys, subscriptionsQueryKeys } from '../../../../../../lib/qu export const useReturnChatToQueueMutation = ( options?: Omit, 'mutationFn'>, ): UseMutationResult => { - const returnChatToQueue = useMethod('livechat:returnAsInquiry'); + const returnChatToQueue = useEndpoint('POST', '/v1/livechat/inquiries.returnAsInquiry'); const queryClient = useQueryClient(); return useMutation({ mutationFn: async (rid) => { - await returnChatToQueue(rid); + await returnChatToQueue({ roomId: rid }); }, ...options, onSuccess: async (data, rid, context) => { diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts index 00d990d97cc19..73d9379dd6534 100644 --- a/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts @@ -1,5 +1,5 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { useMethod } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query'; @@ -8,13 +8,13 @@ import { roomsQueryKeys, subscriptionsQueryKeys } from '../../../../../../lib/qu export const useReturnChatToQueueMutation = ( options?: Omit, 'mutationFn'>, ): UseMutationResult => { - const returnChatToQueue = useMethod('livechat:returnAsInquiry'); + const returnChatToQueue = useEndpoint('POST', '/v1/livechat/inquiries.returnAsInquiry'); const queryClient = useQueryClient(); return useMutation({ mutationFn: async (rid) => { - await returnChatToQueue(rid); + await returnChatToQueue({ roomId: rid }); }, ...options, onSuccess: async (data, rid, context) => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts b/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts index 66b5e19c753da..271b50acb3332 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; import type { Response } from 'supertest'; -import { getCredentials, api, request, credentials, methodCall } from '../../../data/api-data'; +import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { deleteDepartment } from '../../../data/livechat/department'; import { closeOmnichannelRoom, @@ -19,7 +19,7 @@ import { startANewLivechatRoomAndTakeIt, takeInquiry, } from '../../../data/livechat/rooms'; -import { parseMethodResponse, sleep } from '../../../data/livechat/utils'; +import { sleep } from '../../../data/livechat/utils'; import { removePermissionFromAllRoles, restorePermissionToRoles, @@ -324,7 +324,7 @@ describe('LIVECHAT - inquiries', () => { }); }); - describe('livechat:returnAsInquiry', () => { + describe('livechat/inquiries.returnAsInquiry', () => { let testUser: { user: IUser; credentials: Credentials }; before(async () => { const user = await createUser(); @@ -344,59 +344,39 @@ describe('LIVECHAT - inquiries', () => { it('should throw an error if user doesnt have view-l-room permission', async () => { await removePermissionFromAllRoles('view-l-room'); const { body } = await request - .post(methodCall('livechat:returnAsInquiry')) + .post(api('livechat/inquiries.returnAsInquiry')) .set(credentials) - .send({ - message: JSON.stringify({ - method: 'livechat:returnAsInquiry', - params: ['test'], - id: 'id', - msg: 'method', - }), - }) + .send({ roomId: 'test' }) .expect('Content-Type', 'application/json') - .expect(200); - - const response = parseMethodResponse(body); + .expect(403); - expect(response.error.error).to.be.equal('error-not-allowed'); + expect(body).to.have.property('success', false); + expect(body.error).to.have.equal('User does not have the permissions required for this action [error-unauthorized]'); }); it('should fail if provided room doesnt exists', async () => { await restorePermissionToRoles('view-l-room'); const { body } = await request - .post(methodCall('livechat:returnAsInquiry')) + .post(api('livechat/inquiries.returnAsInquiry')) .set(credentials) .send({ - message: JSON.stringify({ - method: 'livechat:returnAsInquiry', - params: ['test'], - id: 'id', - msg: 'method', - }), + roomId: 'test', }) .expect('Content-Type', 'application/json') - .expect(200); + .expect(400); - const response = parseMethodResponse(body); - expect(response.error.error).to.be.equal('error-invalid-room'); + expect(body).to.have.property('success', false); + expect(body).to.have.property('error', 'error-room-not-found'); }); it('should fail if room is not a livechat room', async () => { const { body } = await request - .post(methodCall('livechat:returnAsInquiry')) + .post(api('livechat/inquiries.returnAsInquiry')) .set(credentials) - .send({ - message: JSON.stringify({ - method: 'livechat:returnAsInquiry', - params: ['GENERAL'], - id: 'id', - msg: 'method', - }), - }) + .send({ roomId: 'GENERAL' }) .expect('Content-Type', 'application/json') - .expect(200); + .expect(400); - const response = parseMethodResponse(body); - expect(response.error.error).to.be.equal('error-invalid-room'); + expect(body).to.have.property('success', false); + expect(body).to.have.property('error', 'error-room-not-found'); }); it('should fail if room is closed', async () => { const visitor = await createVisitor(); @@ -404,21 +384,14 @@ describe('LIVECHAT - inquiries', () => { await closeOmnichannelRoom(room._id); const { body } = await request - .post(methodCall('livechat:returnAsInquiry')) + .post(api('livechat/inquiries.returnAsInquiry')) .set(credentials) - .send({ - message: JSON.stringify({ - method: 'livechat:returnAsInquiry', - params: [room._id], - id: 'id', - msg: 'method', - }), - }) + .send({ roomId: room._id }) .expect('Content-Type', 'application/json') - .expect(200); + .expect(400); - const response = parseMethodResponse(body); - expect(response.error.error).to.be.equal('room-closed'); + expect(body).to.have.property('success', false); + expect(body).to.have.property('error', 'room-closed'); }); describe('no serving', () => { let room: IOmnichannelRoom; @@ -431,21 +404,14 @@ describe('LIVECHAT - inquiries', () => { }); it('should fail if no one is serving the room', async () => { const { body } = await request - .post(methodCall('livechat:returnAsInquiry')) + .post(api('livechat/inquiries.returnAsInquiry')) .set(credentials) - .send({ - message: JSON.stringify({ - method: 'livechat:returnAsInquiry', - params: [room._id], - id: 'id', - msg: 'method', - }), - }) + .send({ roomId: room._id }) .expect('Content-Type', 'application/json') .expect(200); - const response = parseMethodResponse(body); - expect(response.result).to.be.false; + expect(body).to.have.property('success', true); + expect(body).to.have.property('result', false); }); }); @@ -460,21 +426,14 @@ describe('LIVECHAT - inquiries', () => { await takeInquiry(inq._id, testUser.credentials); const { body } = await request - .post(methodCall('livechat:returnAsInquiry')) + .post(api('livechat/inquiries.returnAsInquiry')) .set(testUser.credentials) - .send({ - message: JSON.stringify({ - method: 'livechat:returnAsInquiry', - params: [room._id], - id: 'id', - msg: 'method', - }), - }) + .send({ roomId: room._id }) .expect('Content-Type', 'application/json') .expect(200); - const response = parseMethodResponse(body); - expect(response.result).to.be.true; + expect(body).to.have.property('success', true); + expect(body).to.have.property('result', true); }); (IS_EE ? it : it.skip)('should appear on users queued elements', async () => { const { body } = await request @@ -554,16 +513,9 @@ describe('LIVECHAT - inquiries', () => { await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200); await request - .post(methodCall('livechat:returnAsInquiry')) + .post(api('livechat/inquiries.returnAsInquiry')) .set(credentials) - .send({ - message: JSON.stringify({ - method: 'livechat:returnAsInquiry', - params: [room._id], - id: 'id', - msg: 'method', - }), - }) + .send({ roomId: room._id }) .expect('Content-Type', 'application/json') .expect(200); diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 9ae604613810f..164c5624000eb 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -3467,6 +3467,48 @@ const GETLivechatInquiriesGetOneParamsSchema = { export const isGETLivechatInquiriesGetOneParams = ajv.compile(GETLivechatInquiriesGetOneParamsSchema); +type POSTLivechatInquiriesReturnAsInquiry = { + roomId: string; + departmentId?: string; +}; + +const POSTLivechatInquiriesReturnAsInquirySchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + departmentId: { + type: 'string', + nullable: true, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isPOSTLivechatInquiriesReturnAsInquiry = ajv.compile( + POSTLivechatInquiriesReturnAsInquirySchema, +); + +const POSTLivechatInquiriesReturnAsInquirySuccessResponseSchema = { + type: 'object', + properties: { + result: { + type: 'boolean', + }, + success: { + type: 'boolean', + enum: [true], + }, + }, + additionalProperties: false, +}; + +export const POSTLivechatInquiriesReturnAsInquirySuccessResponse = ajv.compile<{ result: boolean }>( + POSTLivechatInquiriesReturnAsInquirySuccessResponseSchema, +); + type GETDashboardTotalizers = { start: string; end: string;