chore!: Remove deprecated livechat `transfer` method and endpoint (#36871)

pull/35380/merge
Martin Schoeler 4 months ago committed by Guilherme Gazzo
parent a1d65f493f
commit 239f4b1171
  1. 10
      .changeset/quiet-bees-turn.md
  2. 111
      apps/meteor/app/livechat/server/api/v1/room.ts
  3. 1
      apps/meteor/app/livechat/server/index.ts
  4. 105
      apps/meteor/app/livechat/server/methods/transfer.ts
  5. 125
      apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts
  6. 4
      packages/ddp-client/src/livechat/LivechatClientImpl.ts
  7. 4
      packages/ddp-client/src/livechat/types/LivechatSDK.ts
  8. 15
      packages/livechat/src/routes/SwitchDepartment/index.tsx
  9. 4
      packages/livechat/src/widget.ts
  10. 15
      packages/rest-typings/src/v1/omnichannel.ts

@ -0,0 +1,10 @@
---
"@rocket.chat/meteor": major
"@rocket.chat/ddp-client": major
"@rocket.chat/livechat": major
"@rocket.chat/rest-typings": major
---
Removes the `livechat:transfer` deprecated method
Removes the `livechat/room.transfer` deprecated endpoint
Creates the `livechat/visitor.department.transfer` for visitors department transfer use

@ -6,13 +6,13 @@ import type {
IUser,
SelectedAgent,
TransferByData,
TransferData,
} from '@rocket.chat/core-typings';
import { isOmnichannelRoom, OmnichannelSourceType } from '@rocket.chat/core-typings';
import { LivechatVisitors, Users, LivechatRooms, Messages } from '@rocket.chat/models';
import { LivechatVisitors, Users, LivechatRooms } from '@rocket.chat/models';
import {
isLiveChatRoomForwardProps,
isPOSTLivechatRoomCloseParams,
isPOSTLivechatRoomTransferParams,
isPOSTLivechatRoomSurveyParams,
isLiveChatRoomJoinProps,
isLiveChatRoomSaveInfoProps,
@ -24,7 +24,9 @@ import {
validateBadRequestErrorResponse,
validateUnauthorizedErrorResponse,
validateForbiddenErrorResponse,
ajv,
} from '@rocket.chat/rest-typings';
import { isPOSTLivechatVisitorDepartmentTransferParams } from '@rocket.chat/rest-typings/src/v1/omnichannel';
import { check } from 'meteor/check';
import { callbacks } from '../../../../../server/lib/callbacks';
@ -236,43 +238,6 @@ API.v1.addRoute(
},
);
API.v1.addRoute(
'livechat/room.transfer',
{ validateParams: isPOSTLivechatRoomTransferParams, deprecation: { version: '7.0.0' } },
{
async post() {
const { rid, token, department } = this.bodyParams;
const guest = await findGuest(token);
if (!guest) {
throw new Error('invalid-token');
}
let room = await findRoom(token, rid);
if (!room) {
throw new Error('invalid-room');
}
// update visited page history to not expire
await Messages.keepHistoryForToken(token);
const { _id, username, name } = guest;
const transferredBy = normalizeTransferredByData({ _id, username, name, userType: 'visitor' }, room);
if (!(await transfer(room, guest, { departmentId: department, transferredBy }))) {
return API.v1.failure();
}
room = await findRoom(token, rid);
if (!room) {
throw new Error('invalid-room');
}
return API.v1.success({ room });
},
},
);
API.v1.addRoute(
'livechat/room.survey',
{ validateParams: isPOSTLivechatRoomSurveyParams },
@ -365,6 +330,74 @@ API.v1.addRoute(
},
);
const livechatVisitorDepartmentTransfer = API.v1.post(
'livechat/visitor/department.transfer',
{
response: {
200: ajv.compile<void>({
type: 'object',
properties: {
success: {
type: 'boolean',
enum: [true],
},
},
required: ['success'],
additionalProperties: false,
}),
400: validateBadRequestErrorResponse,
},
body: isPOSTLivechatVisitorDepartmentTransferParams,
},
async function action() {
const { rid, token, department } = this.bodyParams;
const visitor = await findGuest(token);
if (!visitor) {
return API.v1.failure('invalid-token');
}
const room = await LivechatRooms.findOneById(rid);
if (!room || room.t !== 'l') {
return API.v1.failure('error-invalid-room');
}
if (!room.open) {
return API.v1.failure('This_conversation_is_already_closed');
}
// As this is a visitor endpoint, we should not show the mac limit error
if (!(await Omnichannel.isWithinMACLimit(room))) {
return API.v1.failure('error-transefing-chat');
}
const guest = await LivechatVisitors.findOneEnabledById(room.v?._id);
if (!guest) {
return API.v1.failure('error-invalid-visitor');
}
const transferredBy = normalizeTransferredByData(
{ _id: guest._id, username: guest.username, name: guest.name, userType: 'visitor' },
room,
);
const transferData: TransferData = { transferredBy, departmentId: department };
const chatForwardedResult = await transfer(room, guest, transferData);
if (!chatForwardedResult) {
return API.v1.failure('error-transfering-chat');
}
return API.v1.success();
},
);
type LivechatAnalyticsEndpoints = ExtractRoutesFromAPI<typeof livechatVisitorDepartmentTransfer>;
declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends LivechatAnalyticsEndpoints {}
}
API.v1.addRoute(
'livechat/room.join',
{ authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLiveChatRoomJoinProps },

@ -24,7 +24,6 @@ import './methods/saveCustomField';
import './methods/saveDepartment';
import './methods/sendMessageLivechat';
import './methods/sendFileLivechatMessage';
import './methods/transfer';
import './methods/setUpConnection';
import './methods/takeInquiry';
import './methods/returnAsInquiry';

@ -1,105 +0,0 @@
import { Omnichannel } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { LivechatVisitors, LivechatRooms, Subscriptions, Users } from '@rocket.chat/models';
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';
import { normalizeTransferredByData } from '../lib/Helper';
import { transfer } from '../lib/transfer';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
'livechat:transfer'(transferData: {
roomId: string;
userId?: string;
departmentId?: string;
comment?: string;
clientAction?: boolean;
}): boolean;
}
}
// Deprecated in favor of "livechat/room.forward" endpoint
// TODO: Deprecated: Remove in v6.0.0
Meteor.methods<ServerMethods>({
async 'livechat:transfer'(transferData) {
methodDeprecationLogger.method('livechat:transfer', '7.0.0', '/v1/livechat/room.forward');
const uid = Meteor.userId();
if (!uid || !(await hasPermissionAsync(uid, 'view-l-room'))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:transfer' });
}
check(transferData, {
roomId: String,
userId: Match.Optional(String),
departmentId: Match.Optional(String),
comment: Match.Optional(String),
clientAction: Match.Optional(Boolean),
});
const room = await LivechatRooms.findOneById(transferData.roomId);
if (!room || room.t !== 'l') {
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:transfer' });
}
if (!room.open) {
throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:transfer' });
}
if (!(await Omnichannel.isWithinMACLimit(room))) {
throw new Meteor.Error('error-mac-limit-reached', 'MAC limit reached', { method: 'livechat:transfer' });
}
const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, uid, {
projection: { _id: 1 },
});
if (!subscription && !(await hasPermissionAsync(uid, 'transfer-livechat-guest'))) {
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
method: 'livechat:transfer',
});
}
const guest = await LivechatVisitors.findOneEnabledById(room.v?._id);
if (!guest) {
throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor', { method: 'livechat:transfer' });
}
const user = await Meteor.userAsync();
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:transfer' });
}
const normalizedTransferData: {
roomId: string;
userId?: string;
departmentId?: string;
comment?: string;
clientAction?: boolean;
transferredBy: ReturnType<typeof normalizeTransferredByData>;
transferredTo?: Pick<IUser, '_id' | 'username' | 'name'>;
} = {
...transferData,
transferredBy: normalizeTransferredByData(user, room),
};
if (normalizedTransferData.userId) {
const userToTransfer = await Users.findOneById(normalizedTransferData.userId);
if (!userToTransfer) {
throw new Meteor.Error('error-invalid-user', 'Invalid user to transfer the room');
}
normalizedTransferData.transferredTo = {
_id: userToTransfer._id,
username: userToTransfer.username,
name: userToTransfer.name,
};
}
return transfer(room, guest, normalizedTransferData);
},
});

@ -1895,6 +1895,131 @@ describe('LIVECHAT - rooms', () => {
});
});
describe('livechat/visitor/department.transfer', () => {
let initialDepartmentId: string;
let departmentForwardToId: string;
let omnichannelRoomId: string;
afterEach(async () => {
await Promise.all([
initialDepartmentId && deleteDepartment(initialDepartmentId),
departmentForwardToId && deleteDepartment(departmentForwardToId),
omnichannelRoomId && closeOmnichannelRoom(omnichannelRoomId),
updateSetting('Livechat_Routing_Method', 'Manual_Selection'),
]);
});
it('should not be successful when no target (userId or departmentId) was specified', async () => {
await request
.post(api('livechat/visitor/department.transfer'))
.set(credentials)
.send({
rid: room._id,
token: visitor.token,
department: 'invalid-department-id',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res: Response) => {
expect(res.body).to.have.property('success', false);
});
});
(IS_EE ? it : it.skip)('should return a success message when transferred successfully to a department', async () => {
const { department: initialDepartment } = await createDepartmentWithAnOnlineAgent();
const { department: forwardToDepartment } = await createDepartmentWithAnOnlineAgent();
initialDepartmentId = initialDepartment._id;
departmentForwardToId = forwardToDepartment._id;
const newVisitor = await createVisitor(initialDepartment._id);
const newRoom = await createLivechatRoom(newVisitor.token);
omnichannelRoomId = newRoom._id;
await request
.post(api('livechat/visitor/department.transfer'))
.set(credentials)
.send({
rid: newRoom._id,
token: newVisitor.token,
department: forwardToDepartment._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
const latestRoom = await getLivechatRoomInfo(newRoom._id);
expect(latestRoom).to.have.property('departmentId');
expect(latestRoom.departmentId).to.be.equal(forwardToDepartment._id);
expect(latestRoom).to.have.property('lastMessage');
expect(latestRoom.lastMessage?.t).to.be.equal('livechat_transfer_history');
expect(latestRoom.lastMessage?.u?.username).to.be.equal(newVisitor.username);
expect((latestRoom.lastMessage as any)?.transferData?.scope).to.be.equal('department');
expect((latestRoom.lastMessage as any)?.transferData?.nextDepartment?._id).to.be.equal(forwardToDepartment._id);
});
(IS_EE ? it : it.skip)(
'should return a success message when transferred successfully to an offline department when the department accepts it',
async () => {
const { department: initialDepartment } = await createDepartmentWithAnOnlineAgent();
const { department: forwardToOfflineDepartment } = await createDepartmentWithAnOfflineAgent({ allowReceiveForwardOffline: true });
initialDepartmentId = initialDepartment._id;
departmentForwardToId = forwardToOfflineDepartment._id;
const newVisitor = await createVisitor(initialDepartment._id);
const newRoom = await createLivechatRoom(newVisitor.token);
omnichannelRoomId = newRoom._id;
await request
.post(api('livechat/visitor/department.transfer'))
.set(credentials)
.send({
rid: newRoom._id,
token: newVisitor.token,
department: forwardToOfflineDepartment._id,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
},
);
(IS_EE ? it : it.skip)('inquiry should be taken automatically when agent on department is online again', async () => {
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
const { department: initialDepartment } = await createDepartmentWithAnOnlineAgent();
const { department: forwardToOfflineDepartment } = await createDepartmentWithAnOfflineAgent({ allowReceiveForwardOffline: true });
initialDepartmentId = initialDepartment._id;
departmentForwardToId = forwardToOfflineDepartment._id;
const newVisitor = await createVisitor(initialDepartment._id);
const newRoom = await createLivechatRoom(newVisitor.token);
omnichannelRoomId = newRoom._id;
await request.post(api('livechat/visitor/department.transfer')).set(credentials).send({
rid: newRoom._id,
token: newVisitor.token,
department: forwardToOfflineDepartment._id,
});
await makeAgentAvailable();
const latestRoom = await getLivechatRoomInfo(newRoom._id);
expect(latestRoom).to.have.property('departmentId');
expect(latestRoom.departmentId).to.be.equal(forwardToOfflineDepartment._id);
expect(latestRoom).to.have.property('lastMessage');
expect(latestRoom.lastMessage?.t).to.be.equal('livechat_transfer_history');
expect(latestRoom.lastMessage?.u?.username).to.be.equal(newVisitor.username);
expect((latestRoom.lastMessage as any)?.transferData?.scope).to.be.equal('department');
expect((latestRoom.lastMessage as any)?.transferData?.nextDepartment?._id).to.be.equal(forwardToOfflineDepartment._id);
});
});
describe('livechat/room.survey', () => {
it('should return an "invalid-token" error when the visitor is not found due to an invalid token', async () => {
await request

@ -204,11 +204,11 @@ export class LivechatClientImpl extends DDPSDK implements LivechatStream, Livech
}: {
rid: string;
department: string;
}): Promise<Serialized<OperationResult<'POST', '/v1/livechat/room.transfer'>>> {
}): Promise<Serialized<OperationResult<'POST', '/v1/livechat/visitor/department.transfer'>>> {
if (!this.token) {
throw new Error('Invalid token');
}
return this.rest.post('/v1/livechat/room.transfer', { rid, token: this.token, department });
return this.rest.post('/v1/livechat/visitor/department.transfer', { rid, token: this.token, department });
}
async grantVisitor(

@ -55,8 +55,8 @@ export interface LivechatEndpoints {
// POST
transferChat(
args: OperationParams<'POST', '/v1/livechat/room.transfer'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/room.transfer'>>>;
args: OperationParams<'POST', '/v1/livechat/visitor/department.transfer'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/visitor/department.transfer'>>>;
grantVisitor(
guest: OperationParams<'POST', '/v1/livechat/visitor'>,
): Promise<Serialized<OperationResult<'POST', '/v1/livechat/visitor'>>>;

@ -1,4 +1,3 @@
import type { IOmnichannelRoom, Serialized } from '@rocket.chat/core-typings';
import { useContext } from 'preact/hooks';
import { route } from 'preact-router';
import { Controller, useForm } from 'react-hook-form';
@ -76,18 +75,8 @@ const SwitchDepartment = (_: SwitchDepartmentProps) => {
try {
const { _id: rid } = room;
const result = await Livechat.transferChat({ rid, department });
// TODO: Investigate why the api results are not returning the correct type
const { success } = result as Serialized<
| {
room: IOmnichannelRoom;
success: boolean;
}
| {
room: IOmnichannelRoom;
success: boolean;
warning: string;
}
>;
const { success } = result;
if (!success) {
throw t('no_available_agents_to_transfer');
}

@ -374,8 +374,8 @@ function setParentUrl(url: string) {
callHook('setParentUrl', url);
}
function transferChat(department: string) {
callHook('transferChat', department);
function transferChat(rid: string, department: string) {
callHook('transferChat', rid, department);
}
function setGuestMetadata(metadata: StoreState['iframe']['guestMetadata']) {

@ -37,7 +37,6 @@ import { ILivechatAgentStatus } from '@rocket.chat/core-typings';
import type { WithId } from 'mongodb';
import { ajv } from './Ajv';
import type { Deprecated } from '../helpers/Deprecated';
import type { PaginatedRequest } from '../helpers/PaginatedRequest';
import type { PaginatedResult } from '../helpers/PaginatedResult';
@ -2913,13 +2912,13 @@ const POSTLivechatRoomCloseByUserParamsSchema = {
export const isPOSTLivechatRoomCloseByUserParams = ajv.compile<POSTLivechatRoomCloseByUserParams>(POSTLivechatRoomCloseByUserParamsSchema);
type POSTLivechatRoomTransferParams = {
type POSTLivechatVisitorDepartmentTransferParams = {
token: string;
rid: string;
department: string;
};
const POSTLivechatRoomTransferParamsSchema = {
const POSTLivechatVisitorDepartmentTransferParamsSchema = {
type: 'object',
properties: {
token: {
@ -2936,7 +2935,9 @@ const POSTLivechatRoomTransferParamsSchema = {
additionalProperties: false,
};
export const isPOSTLivechatRoomTransferParams = ajv.compile<POSTLivechatRoomTransferParams>(POSTLivechatRoomTransferParamsSchema);
export const isPOSTLivechatVisitorDepartmentTransferParams = ajv.compile<POSTLivechatVisitorDepartmentTransferParams>(
POSTLivechatVisitorDepartmentTransferParamsSchema,
);
type POSTLivechatRoomSurveyParams = {
token: string;
@ -4614,7 +4615,7 @@ export type OmnichannelEndpoints = {
GET: (params: LiveChatRoomJoin) => void;
};
'/v1/livechat/room.forward': {
POST: (params: LiveChatRoomForward) => void;
POST: (params: LiveChatRoomForward) => { success: boolean };
};
'/v1/livechat/room.saveInfo': {
POST: (params: LiveChatRoomSaveInfo) => void;
@ -4944,8 +4945,8 @@ export type OmnichannelEndpoints = {
'/v1/livechat/room.closeByUser': {
POST: (params: POSTLivechatRoomCloseByUserParams) => void;
};
'/v1/livechat/room.transfer': {
POST: (params: POSTLivechatRoomTransferParams) => Deprecated<{ room: IOmnichannelRoom }>;
'/v1/livechat/visitor/department.transfer': {
POST: (params: POSTLivechatVisitorDepartmentTransferParams) => { success: boolean };
};
'/v1/livechat/room.survey': {
POST: (params: POSTLivechatRoomSurveyParams) => { rid: string; data: unknown };

Loading…
Cancel
Save