Skip to content

Commit

Permalink
Chore: Migrate omni-chat forwarding to use API instead of meteor meth…
Browse files Browse the repository at this point in the history
…od (#26377)
  • Loading branch information
murtaza98 authored and csuarez committed Aug 26, 2022
1 parent d17c17a commit 14a73ea
Show file tree
Hide file tree
Showing 12 changed files with 320 additions and 67 deletions.
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

0 comments on commit 14a73ea

Please sign in to comment.