Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore: Migrate omni-chat forwarding to use API instead of meteor method #26377

Merged
merged 13 commits into from
Aug 16, 2022
Merged
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