Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ API.v1.addRoute(
'livechat/department/:_id/agents',
{
authRequired: true,
// TODO: Use AJV
Comment thread
KevLehman marked this conversation as resolved.
Outdated
Comment thread
KevLehman marked this conversation as resolved.
Outdated
Comment thread
KevLehman marked this conversation as resolved.
Outdated
permissionsRequired: {
GET: { permissions: ['view-livechat-departments', 'view-l-room'], operation: 'hasAny' },
POST: { permissions: ['manage-livechat-departments', 'add-livechat-department-agents'], operation: 'hasAny' },
Expand Down
39 changes: 9 additions & 30 deletions apps/meteor/app/livechat/imports/server/rest/users.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Users } from '@rocket.chat/models';
import { isLivechatUsersManagerGETProps, isPOSTLivechatUsersTypeProps } from '@rocket.chat/rest-typings';
import { check } from 'meteor/check';
import _ from 'underscore';

import { API } from '../../../../api/server';
import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems';
Expand Down Expand Up @@ -96,45 +95,25 @@ API.v1.addRoute(
{ authRequired: true, permissionsRequired: ['view-livechat-manager'] },
{
async get() {
const user = await Users.findOneById(this.urlParams._id);

if (!user) {
return API.v1.failure('User not found');
}

let role;

if (this.urlParams.type === 'agent') {
role = 'livechat-agent';
} else if (this.urlParams.type === 'manager') {
role = 'livechat-manager';
} else {
if (!['agent', 'manager'].includes(this.urlParams.type)) {
throw new Error('Invalid type');
}
const role = this.urlParams.type === 'agent' ? 'livechat-agent' : 'livechat-manager';

if (user.roles.indexOf(role) !== -1) {
return API.v1.success({
user: _.pick(user, '_id', 'username', 'name', 'status', 'statusLivechat', 'emails', 'livechat'),
});
}

return API.v1.success({
user: null,
const user = await Users.findOneByIdAndRole(this.urlParams._id, role, {
projection: { _id: 1, username: 1, name: 1, status: 1, statusLivechat: 1, emails: 1, livechat: 1 },
});

// TODO: throw error instead of returning null
return API.v1.success({ user });
},
async delete() {
const user = await Users.findOneById(this.urlParams._id);

if (!user?.username) {
return API.v1.failure();
}

if (this.urlParams.type === 'agent') {
if (await removeAgent(user.username)) {
if (await removeAgent(this.urlParams._id)) {
return API.v1.success();
}
} else if (this.urlParams.type === 'manager') {
if (await removeManager(user.username)) {
if (await removeManager(this.urlParams._id)) {
return API.v1.success();
}
} else {
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/app/livechat/server/api/v1/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@ API.v1.addRoute('livechat/visitor/:token', {
throw new Meteor.Error('visitor-has-open-rooms', 'Cannot remove visitors with opened rooms');
}

const { _id } = visitor;
const result = await removeGuest(_id);
const { _id, token } = visitor;
const result = await removeGuest({ _id, token });
Comment thread
KevLehman marked this conversation as resolved.
Comment thread
KevLehman marked this conversation as resolved.
if (!result.modifiedCount) {
throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor');
}
Expand Down
5 changes: 3 additions & 2 deletions apps/meteor/app/livechat/server/lib/QueueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import type {
import { LivechatInquiryStatus } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import type { InsertionModel } from '@rocket.chat/model-typings';
import { LivechatContacts, LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models';
import { LivechatContacts, LivechatDepartment, LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models';
import { Random } from '@rocket.chat/random';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { createLivechatRoom, createLivechatInquiry, allowAgentSkipQueue, prepareLivechatRoom } from './Helper';
import { RoutingManager } from './RoutingManager';
import { isVerifiedChannelInSource } from './contacts/isVerifiedChannelInSource';
import { checkOnlineForDepartment } from './departmentsLib';
import { checkOnlineAgents, getOnlineAgents } from './service-status';
import { getInquirySortMechanismSetting } from './settings';
import { dispatchInquiryPosition } from '../../../../ee/app/livechat-enterprise/server/lib/Helper';
Expand Down Expand Up @@ -69,7 +70,7 @@ const getDepartment = async (department: string): Promise<string | undefined> =>
return;
}

if (await LivechatDepartmentAgents.checkOnlineForDepartment(department)) {
if (await checkOnlineForDepartment(department)) {
return department;
}

Expand Down
95 changes: 50 additions & 45 deletions apps/meteor/app/livechat/server/lib/departmentsLib.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { AppEvents, Apps } from '@rocket.chat/apps';
import type { LivechatDepartmentDTO, ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatDepartmentAgents, LivechatVisitors, LivechatRooms } from '@rocket.chat/models';
import type { LivechatDepartmentDTO, ILivechatDepartment, ILivechatDepartmentAgents, ILivechatAgent } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatDepartmentAgents, LivechatVisitors, LivechatRooms, Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { updateDepartmentAgents } from './Helper';
import { afterDepartmentArchived, afterDepartmentUnarchived } from './hooks';
import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable';
import { livechatLogger } from './logger';
import { callbacks } from '../../../../lib/callbacks';
import {
notifyOnLivechatDepartmentAgentChangedByDepartmentId,
notifyOnLivechatDepartmentAgentChanged,
} from '../../../lib/server/lib/notifyListener';
import { settings } from '../../../settings/server';
/**
* @param {string|null} _id - The department id
* @param {Partial<import('@rocket.chat/core-typings').ILivechatDepartment>} departmentData
Expand All @@ -27,15 +29,16 @@ export async function saveDepartment(
},
departmentUnit?: { _id?: string },
) {
check(_id, Match.Maybe(String));
if (departmentUnit?._id !== undefined && typeof departmentUnit._id !== 'string') {
throw new Meteor.Error('error-invalid-department-unit', 'Invalid department unit id provided', {
method: 'livechat:saveDepartment',
});
}

const department = _id
? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1, parentId: 1 } })
? await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'archived' | 'enabled' | 'parentId'>>(_id, {
projection: { _id: 1, archived: 1, enabled: 1, parentId: 1 },
})
: null;

if (departmentUnit && !departmentUnit._id && department && department.parentId) {
Expand All @@ -59,6 +62,7 @@ export async function saveDepartment(
});
}

// TODO: Use AJV or Zod for validation (or the lib we are using rn)
const defaultValidations: Record<string, Match.Matcher<any> | BooleanConstructor | StringConstructor> = {
enabled: Boolean,
name: String,
Expand Down Expand Up @@ -112,8 +116,8 @@ export async function saveDepartment(
}

if (fallbackForwardDepartment) {
const fallbackDep = await LivechatDepartment.findOneById(fallbackForwardDepartment, {
projection: { _id: 1, fallbackForwardDepartment: 1 },
const fallbackDep = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id'>>(fallbackForwardDepartment, {
projection: { _id: 1 },
});
if (!fallbackDep) {
throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', {
Expand Down Expand Up @@ -156,11 +160,7 @@ export async function archiveDepartment(_id: string) {
throw new Error('department-not-found');
}

await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]);

void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id);

await callbacks.run('livechat.afterDepartmentArchived', department);
await afterDepartmentArchived(department);
}

export async function unarchiveDepartment(_id: string) {
Expand All @@ -170,12 +170,7 @@ export async function unarchiveDepartment(_id: string) {
throw new Meteor.Error('department-not-found');
}

// TODO: these kind of actions should be on events instead of here
await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]);

void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id);

return true;
await afterDepartmentUnarchived(department);
}

export async function saveDepartmentAgents(
Expand All @@ -185,6 +180,7 @@ export async function saveDepartmentAgents(
remove?: Pick<ILivechatDepartmentAgents, 'agentId'>[];
},
) {
// TODO: remove when endpoint is validated
Comment thread
KevLehman marked this conversation as resolved.
Outdated
check(_id, String);
check(departmentAgents, {
upsert: Match.Maybe([
Expand Down Expand Up @@ -213,28 +209,20 @@ export async function saveDepartmentAgents(
return updateDepartmentAgents(_id, departmentAgents, department.enabled);
}

export async function setDepartmentForGuest({ token, department }: { token: string; department: string }) {
check(token, String);
check(department, String);

livechatLogger.debug(`Switching departments for user with token ${token} (to ${department})`);

const updateUser = {
$set: {
department,
},
};
export async function setDepartmentForGuest({ visitorId, department }: { visitorId: string; department: string }) {
livechatLogger.debug({
msg: 'Switching departments for visitor',
visitorId,
department,
});

const dep = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id'>>(department, { projection: { _id: 1 } });
if (!dep) {
throw new Meteor.Error('invalid-department', 'Provided department does not exists');
}

const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } });
if (!visitor) {
throw new Meteor.Error('invalid-token', 'Provided token is invalid');
}
await LivechatVisitors.updateById(visitor._id, updateUser);
// Visitor is already validated at this point
return LivechatVisitors.updateDepartmentById(visitorId, department);
}

export async function removeDepartment(departmentId: string) {
Expand All @@ -260,10 +248,8 @@ export async function removeDepartment(departmentId: string) {
`Performing post-department-removal actions: ${_id}. Removing department agents, unsetting fallback department and removing department from rooms`,
);

const removeByDept = LivechatDepartmentAgents.removeByDepartmentId(_id);

const promiseResponses = await Promise.allSettled([
removeByDept,
LivechatDepartmentAgents.removeByDepartmentId(_id),
LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id),
LivechatRooms.bulkRemoveDepartmentAndUnitsFromRooms(_id),
]);
Expand Down Expand Up @@ -296,19 +282,38 @@ export async function removeDepartment(departmentId: string) {
}

export async function getRequiredDepartment(onlineRequired = true) {
const departments = LivechatDepartment.findEnabledWithAgents();
if (!onlineRequired) {
return LivechatDepartment.findOneEnabledWithAgentsAndRegistration();
}

for await (const dept of departments) {
if (!dept.showOnRegistration) {
continue;
}
if (!onlineRequired) {
return dept;
}
const departments = LivechatDepartment.findEnabledWithAgentsAndRegistration();

const onlineAgents = await LivechatDepartmentAgents.countOnlineForDepartment(dept._id);
for await (const dept of departments) {
const departmentAgents = await LivechatDepartmentAgents.findByDepartmentId(dept._id, { projection: { username: 1 } }).toArray();
const onlineAgents = await Users.countOnlineUserFromList(
departmentAgents.map((a) => a.username),
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
);
if (onlineAgents) {
return dept;
}
}
}

export async function checkOnlineForDepartment(departmentId: string) {
const depUsers = await LivechatDepartmentAgents.findByDepartmentId(departmentId, { projection: { username: 1 } }).toArray();
const onlineForDep = await Users.findOneOnlineAgentByUserList(
depUsers.map((agent) => agent.username),
{ projection: { _id: 1 } },
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
);

return !!onlineForDep;
}

export async function getOnlineForDepartment(departmentId: string) {
const agents = await LivechatDepartmentAgents.findByDepartmentId(departmentId, { projection: { username: 1 } }).toArray();
const usernames = agents.map(({ username }) => username);

return Users.findOnlineUserFromList<ILivechatAgent>([...new Set(usernames)], settings.get<boolean>('Livechat_enabled_when_agent_idle'));
}
53 changes: 20 additions & 33 deletions apps/meteor/app/livechat/server/lib/guests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
import type { ILivechatVisitor, IOmnichannelRoom, UserStatus } from '@rocket.chat/core-typings';
import type { ILivechatVisitor, IOmnichannelRoom, IUser, UserStatus } from '@rocket.chat/core-typings';
import {
LivechatVisitors,
LivechatCustomField,
Expand Down Expand Up @@ -42,25 +42,12 @@ export async function saveGuest(
}

livechatLogger.debug({ msg: 'Saving guest', guestData });
const updateData: {
name?: string | undefined;
username?: string | undefined;
email?: string | undefined;
phone?: string | undefined;
livechatData: {
[k: string]: any;
};
} = { livechatData: {} };

if (name) {
updateData.name = name;
}
if (email) {
updateData.email = email;
}
if (phone) {
updateData.phone = phone;
}
const updateData = {
...(name && { name }),
...(email && { email }),
...(phone && { phone }),
...(livechatData && { livechatData: {} }),
Comment thread
KevLehman marked this conversation as resolved.
Outdated
};

const customFields: Record<string, any> = {};

Expand Down Expand Up @@ -91,13 +78,8 @@ export async function saveGuest(
return ret;
}

export async function removeGuest(_id: string) {
const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } });
if (!guest) {
throw new Error('error-invalid-guest');
}

await cleanGuestHistory(guest.token);
export async function removeGuest({ _id, token }: { _id: string; token: string }) {
await cleanGuestHistory(token);
return LivechatVisitors.disableById(_id);
}

Expand Down Expand Up @@ -147,7 +129,8 @@ async function cleanGuestHistory(token: string) {
throw new Error('error-invalid-guest');
}

const cursor = LivechatRooms.findByVisitorToken(token);
// TODO: optimize function => instead of removing one by one, fetch the _ids of the rooms and then remove them in bulk
const cursor = LivechatRooms.findByVisitorToken(token, { projection: { _id: 1 } });
for await (const room of cursor) {
await Promise.all([
Subscriptions.removeByRoomId(room._id, {
Expand All @@ -174,7 +157,11 @@ export async function getLivechatRoomGuestInfo(room: IOmnichannelRoom) {
throw new Error('error-invalid-visitor');
}

const agent = room.servedBy?._id ? await Users.findOneById(room.servedBy?._id) : null;
const agent = room.servedBy?._id
? await Users.findOneById<Pick<IUser, '_id' | 'customFields' | 'name' | 'username' | 'emails'>>(room.servedBy?._id, {
projection: { _id: 1, customFields: 1, name: 1, username: 1, emails: 1 },
})
: null;

const ua = new UAParser();
ua.setUA(visitor.userAgent || '');
Expand Down Expand Up @@ -230,10 +217,10 @@ export async function getLivechatRoomGuestInfo(room: IOmnichannelRoom) {
}

export async function notifyGuestStatusChanged(token: string, status: UserStatus) {
// TODO: a promise.all maybe?
await LivechatRooms.updateVisitorStatus(token, status);

const inquiryVisitorStatus = await LivechatInquiry.updateVisitorStatus(token, status);
const [, inquiryVisitorStatus] = await Promise.all([
LivechatRooms.updateVisitorStatus(token, status),
LivechatInquiry.updateVisitorStatus(token, status),
]);

if (inquiryVisitorStatus.modifiedCount) {
void notifyOnLivechatInquiryChangedByToken(token, 'updated', { v: { status } });
Expand Down
Loading