Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions apps/meteor/app/livechat/server/api/v1/room.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Match, check } from 'meteor/check';
import { Random } from 'meteor/random';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { OmnichannelSourceType } from '@rocket.chat/core-typings';
import { LivechatVisitors, Users } from '@rocket.chat/models';
import { isLiveChatRoomForwardProps } from '@rocket.chat/rest-typings';

import { settings as rcSettings } from '../../../../settings/server';
import { Messages, LivechatRooms } from '../../../../models/server';
Expand Down Expand Up @@ -195,10 +197,34 @@ API.v1.addRoute('livechat/room.survey', {

API.v1.addRoute(
'livechat/room.forward',
{ authRequired: true },
{ authRequired: true, permissionsRequired: ['view-l-room', 'transfer-livechat-guest'], validateParams: isLiveChatRoomForwardProps },
{
post() {
API.v1.success(Meteor.runAsUser(this.userId, () => Meteor.call('livechat:transfer', this.bodyParams)));
async post() {
const transferData = this.bodyParams;

const room = await LivechatRooms.findOneById(this.bodyParams.roomId);
if (!room || room.t !== 'l') {
throw new Error('error-invalid-room', 'Invalid room');
}

if (!room.open) {
throw new Error('This_conversation_is_already_closed');
}

const guest = await LivechatVisitors.findOneById(room.v && room.v._id);
transferData.transferredBy = normalizeTransferredByData(Meteor.user() || {}, room);
if (transferData.userId) {
const userToTransfer = await Users.findOneById(transferData.userId);
transferData.transferredTo = {
_id: userToTransfer._id,
username: userToTransfer.username,
name: userToTransfer.name,
};
}

const chatForwardedResult = await Livechat.transfer(room, guest, transferData);

return chatForwardedResult ? API.v1.success() : API.v1.failure();
},
},
);
Expand Down
28 changes: 7 additions & 21 deletions apps/meteor/app/livechat/server/lib/Helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,24 +349,18 @@ export const forwardRoomToAgent = async (room, transferData) => {
const user = Users.findOneOnlineAgentById(agentId);
if (!user) {
logger.debug(`Agent ${agentId} is offline. Cannot forward`);
throw new Meteor.Error('error-user-is-offline', 'User is offline', {
function: 'forwardRoomToAgent',
});
throw new Error('error-user-is-offline');
}

const { _id: rid, servedBy: oldServedBy } = room;
const inquiry = LivechatInquiry.findOneByRoomId(rid);
if (!inquiry) {
logger.debug(`No inquiries found for room ${room._id}. Cannot forward`);
throw new Meteor.Error('error-invalid-inquiry', 'Invalid inquiry', {
function: 'forwardRoomToAgent',
});
throw new Error('error-invalid-inquiry');
}

if (oldServedBy && agentId === oldServedBy._id) {
throw new Meteor.Error('error-selected-agent-room-agent-are-same', 'The selected agent and the room agent are the same', {
function: 'forwardRoomToAgent',
});
throw new Error('error-selected-agent-room-agent-are-same');
}

const { username } = user;
Expand Down Expand Up @@ -445,32 +439,24 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => {
const inquiry = LivechatInquiry.findOneByRoomId(rid);
if (!inquiry) {
logger.debug(`Cannot forward room ${room._id}. No inquiries found`);
throw new Meteor.Error('error-transferring-inquiry');
throw new Error('error-transferring-inquiry');
}

const { departmentId } = transferData;
if (oldDepartmentId === departmentId) {
throw new Meteor.Error(
'error-forwarding-chat-same-department',
'The selected department and the current room department are the same',
{ function: 'forwardRoomToDepartment' },
);
throw new Error('error-forwarding-chat-same-department');
}

const { userId: agentId, clientAction } = transferData;
if (agentId) {
logger.debug(`Forwarding room ${room._id} to department ${departmentId} (to user ${agentId})`);
let user = Users.findOneOnlineAgentById(agentId);
if (!user) {
throw new Meteor.Error('error-user-is-offline', 'User is offline', {
function: 'forwardRoomToAgent',
});
throw new Error('error-user-is-offline');
}
user = LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(agentId, departmentId);
if (!user) {
throw new Meteor.Error('error-user-not-belong-to-department', 'The selected user does not belong to this department', {
function: 'forwardRoomToDepartment',
});
throw new Error('error-user-not-belong-to-department');
}
const { username } = user;
agent = { agentId, username };
Expand Down
19 changes: 17 additions & 2 deletions apps/meteor/app/livechat/server/lib/Livechat.js
Original file line number Diff line number Diff line change
Expand Up @@ -710,14 +710,29 @@ export const Livechat = {
},
};

return Messages.createTransferHistoryWithRoomIdMessageAndUser(room._id, '', { _id, username }, transfer);
const type = 'livechat_transfer_history';
const transferMessage = {
t: type,
rid: room._id,
ts: new Date(),
msg: '',
u: {
_id,
username,
},
groupable: false,
};

Object.assign(transferMessage, transfer);

sendMessage(transferredBy, transferMessage, room);
},

async transfer(room, guest, transferData) {
Livechat.logger.debug(`Transfering room ${room._id} [Transfered by: ${transferData?.transferredBy?._id}]`);
if (room.onHold) {
Livechat.logger.debug('Cannot transfer. Room is on hold');
throw new Meteor.Error('error-room-onHold', 'Room On Hold', { method: 'livechat:transfer' });
throw new Error('error-room-onHold');
}

if (transferData.departmentId) {
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/app/livechat/server/methods/transfer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { LivechatRooms, Subscriptions, Users } from '../../../models/server';
import { Livechat } from '../lib/Livechat';
import { normalizeTransferredByData } from '../lib/Helper';

// Deprecated in favor of "livechat/room.forward" endpoint
// TODO: Deprecated: Remove in v6.0.0
Meteor.methods({
async 'livechat:transfer'(transferData) {
if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-l-room')) {
Expand Down
23 changes: 0 additions & 23 deletions apps/meteor/app/models/server/models/Messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -818,29 +818,6 @@ export class Messages extends Base {
return record;
}

createTransferHistoryWithRoomIdMessageAndUser(roomId, message, user, extraData) {
const type = 'livechat_transfer_history';
const record = {
t: type,
rid: roomId,
ts: new Date(),
msg: message,
u: {
_id: user._id,
username: user.username,
},
groupable: false,
};

if (settings.get('Message_Read_Receipt_Enabled')) {
record.unread = true;
}
Object.assign(record, extraData);

record._id = this.insertOrUpsert(record);
return record;
}

createTranscriptHistoryWithRoomIdMessageAndUser(roomId, message, user, extraData) {
const type = 'livechat_transcript_history';
const record = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export const useQuickActions = (
}
}, [closeModal, discardTranscript, dispatchToastMessage, rid, t]);

const forwardChat = useMethod('livechat:transfer');
const forwardChat = useEndpoint('POST', '/v1/livechat/room.forward');

const handleForwardChat = useCallback(
async (departmentId?: string, userId?: string, comment?: string) => {
Expand Down Expand Up @@ -173,7 +173,7 @@ export const useQuickActions = (
FlowRouter.go('/');
closeModal();
} catch (error: any) {
handleError(error);
dispatchToastMessage({ type: 'error', message: error as any });
}
},
[closeModal, dispatchToastMessage, forwardChat, rid, t],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Meteor } from 'meteor/meteor';
import { Users } from '@rocket.chat/models';

import { callbacks } from '../../../../../lib/callbacks';
Expand Down Expand Up @@ -73,7 +72,7 @@ callbacks.add(
cbLogger.debug('Callback with error. Agent reached max amount of simultaneous chats');
callbacks.run('livechat.onMaxNumberSimultaneousChatsReached', inquiry);
if (options.clientAction && !options.forwardingToDepartment) {
throw new Meteor.Error('error-max-number-simultaneous-chats-reached', 'Not allowed');
throw new Error('error-max-number-simultaneous-chats-reached');
}

return null;
Expand Down
65 changes: 62 additions & 3 deletions apps/meteor/tests/data/livechat/department.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import { api, credentials, request } from '../api-data';
import faker from '@faker-js/faker';
import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings';
import { api, credentials, methodCall, request } from '../api-data';
import { password } from '../user';
import { createUser, login } from '../users.helper';
import { createAgent, makeAgentAvailable } from './rooms';
import { DummyResponse } from './utils';

export const createDepartment = (): Promise<ILivechatDepartment> =>
new Promise((resolve, reject) => {
Expand All @@ -16,10 +21,64 @@ export const createDepartment = (): Promise<ILivechatDepartment> =>
},
})
.set(credentials)
.end((err, res) => {
.end((err: Error, res: DummyResponse<ILivechatDepartment>) => {
if (err) {
return reject(err);
}
resolve(res.body.department);
});
});

export const createDepartmentWithMethod = (initialAgents: { agentId: string, username: string }[] = []) =>
new Promise((resolve, reject) => {
request
.post(methodCall('livechat:saveDepartment'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:saveDepartment',
params: ['', {
enabled: true,
email: faker.internet.email(),
showOnRegistration: true,
showOnOfflineForm: true,
name: `new department ${Date.now()}`,
description: 'created from api',
}, initialAgents],
id: 'id',
msg: 'method',
}),
})
.end((err: any, res: any) => {
if (err) {
return reject(err);
}
resolve(JSON.parse(res.body.message).result);
});
});

export const createDepartmentWithAnOnlineAgent = async (): Promise<{department: ILivechatDepartment, agent: IUser}> => {
const agent: IUser = await createUser();
const createdUserCredentials = await login(agent.username, password);
await createAgent(agent.username);
await makeAgentAvailable(createdUserCredentials);

const department = await createDepartmentWithMethod() as ILivechatDepartment;

await addOrRemoveAgentFromDepartment(department._id, {agentId: agent._id, username: (agent.username as string)}, true);

return {
department,
agent,
};
};

export const addOrRemoveAgentFromDepartment = async (departmentId: string, agent: { agentId: string; username: string; count?: number; order?: number }, add: boolean) => {
const response = await request.post(api('livechat/department/' + departmentId + '/agents')).set(credentials).send({
...add ? { upsert: [agent], remove: [] } : { remove: [agent], upsert: [] },
});

if (response.status !== 200) {
throw new Error('Failed to add or remove agent from department. Status code: ' + response.status + '\n' + response.body);
}
}
31 changes: 22 additions & 9 deletions apps/meteor/tests/data/livechat/rooms.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import type { IInquiry, ILivechatAgent, ILivechatDepartment, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { api, credentials, methodCall, request } from '../api-data';
import { adminUsername } from '../user';

type DummyResponse<T, E = 'wrapped'> =
E extends 'wrapped' ? { body: { [k: string]: T } } : { body: T };
import { DummyResponse } from './utils';

export const createLivechatRoom = (visitorToken: string): Promise<IOmnichannelRoom> =>
new Promise((resolve) => {
Expand All @@ -13,7 +11,7 @@ export const createLivechatRoom = (visitorToken: string): Promise<IOmnichannelRo
.end((_err: Error, res: DummyResponse<IOmnichannelRoom>) => resolve(res.body.room));
});

export const createVisitor = (): Promise<ILivechatVisitor> =>
export const createVisitor = (department?: string): Promise<ILivechatVisitor> =>
new Promise((resolve, reject) => {
const token = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const email = `${token}@${token}.com`;
Expand All @@ -32,6 +30,7 @@ export const createVisitor = (): Promise<ILivechatVisitor> =>
token,
phone,
customFields: [{ key: 'address', value: 'Rocket.Chat street', overwrite: true }],
...(department ? { department } : {}),
},
})
.end((err: Error, res: DummyResponse<ILivechatVisitor>) => {
Expand Down Expand Up @@ -94,13 +93,13 @@ export const createDepartment = (agents?: { agentId: string }[]): Promise<ILivec
});
}

export const createAgent = (): Promise<ILivechatAgent> =>
export const createAgent = (overrideUsername?: string): Promise<ILivechatAgent> =>
new Promise((resolve, reject) => {
request
.post(api('livechat/users/agent'))
.set(credentials)
.send({
username: adminUsername,
username: overrideUsername || adminUsername,
})
.end((err: Error, res: DummyResponse<ILivechatAgent>) => {
if (err) {
Expand All @@ -126,9 +125,9 @@ export const createManager = (): Promise<ILivechatAgent> =>
});
});

export const makeAgentAvailable = (): Promise<unknown> =>
export const makeAgentAvailable = (overrideCredentials?: { 'X-Auth-Token': string; 'X-User-Id': string }): Promise<unknown> =>
new Promise((resolve, reject) => {
request.post(api('users.setStatus')).set(credentials).send({
request.post(api('users.setStatus')).set(overrideCredentials || credentials).send({
message: '',
status: 'online',
}).end((err: Error, _res: DummyResponse<unknown, 'unwrapped'>) => {
Expand All @@ -137,7 +136,7 @@ export const makeAgentAvailable = (): Promise<unknown> =>
}
request
.post(methodCall('livechat/changeLivechatStatus'))
.set(credentials)
.set(overrideCredentials || credentials)
.send({
message: JSON.stringify({
method: 'livechat/changeLivechatStatus',
Expand All @@ -155,3 +154,17 @@ export const makeAgentAvailable = (): Promise<unknown> =>
});
});


export const getLivechatRoomInfo = (roomId: string): Promise<IOmnichannelRoom> => {
return new Promise((resolve /* , reject*/) => {
request
.get(api('channels.info'))
.set(credentials)
.query({
roomId,
})
.end((_err: Error, req: DummyResponse<IOmnichannelRoom>) => {
resolve(req.body.channel);
});
});
}
2 changes: 2 additions & 0 deletions apps/meteor/tests/data/livechat/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type DummyResponse<T, E = 'wrapped'> =
E extends 'wrapped' ? { body: { [k: string]: T } } : { body: T };
Loading