refactor: remove meteor call from livechat units (server) (#35860)

Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz>
pull/35949/head^2
Marcos Spessatto Defendi 8 months ago committed by GitHub
parent 5861af4727
commit cefafb0589
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts
  2. 9
      apps/meteor/app/lib/server/functions/setUserActiveStatus.ts
  3. 1
      apps/meteor/app/livechat/imports/server/rest/rooms.ts
  4. 21
      apps/meteor/app/livechat/server/api/lib/livechat.ts
  5. 4
      apps/meteor/app/livechat/server/api/lib/rooms.ts
  6. 2
      apps/meteor/app/livechat/server/api/lib/visitors.ts
  7. 2
      apps/meteor/app/livechat/server/api/v1/agent.ts
  8. 2
      apps/meteor/app/livechat/server/api/v1/config.ts
  9. 2
      apps/meteor/app/livechat/server/api/v1/contact.ts
  10. 2
      apps/meteor/app/livechat/server/api/v1/message.ts
  11. 2
      apps/meteor/app/livechat/server/api/v1/statistics.ts
  12. 6
      apps/meteor/app/livechat/server/api/v1/visitor.ts
  13. 3
      apps/meteor/app/livechat/server/lib/analytics/dashboards.ts
  14. 15
      apps/meteor/app/livechat/server/lib/contacts/registerContact.ts
  15. 2
      apps/meteor/app/livechat/server/methods/getAnalyticsChartData.ts
  16. 2
      apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts
  17. 7
      apps/meteor/ee/app/livechat-enterprise/server/api/lib/units.ts
  18. 10
      apps/meteor/ee/app/livechat-enterprise/server/api/reports.ts
  19. 30
      apps/meteor/ee/app/livechat-enterprise/server/api/sla.ts
  20. 7
      apps/meteor/ee/app/livechat-enterprise/server/api/units.ts
  21. 13
      apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts
  22. 14
      apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts
  23. 1
      apps/meteor/ee/app/livechat-enterprise/server/index.ts
  24. 7
      apps/meteor/ee/app/livechat-enterprise/server/lib/Helper.ts
  25. 33
      apps/meteor/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.ts
  26. 4
      apps/meteor/ee/app/livechat-enterprise/server/lib/SlaHelper.ts
  27. 14
      apps/meteor/ee/app/livechat-enterprise/server/lib/restrictQuery.ts
  28. 16
      apps/meteor/ee/app/livechat-enterprise/server/lib/unit.ts
  29. 19
      apps/meteor/ee/app/livechat-enterprise/server/lib/units.ts
  30. 32
      apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts
  31. 2
      apps/meteor/ee/app/livechat-enterprise/server/methods/removeUnit.ts
  32. 2
      apps/meteor/ee/app/livechat-enterprise/server/methods/saveUnit.ts
  33. 6
      apps/meteor/ee/server/lib/audit/methods.ts
  34. 45
      apps/meteor/ee/server/models/raw/LivechatUnit.ts
  35. 8
      apps/meteor/lib/callbacks.ts
  36. 2
      apps/meteor/server/methods/setUserActiveStatus.ts
  37. 18
      apps/meteor/server/services/omnichannel-analytics/service.ts
  38. 3
      packages/core-services/src/types/IOmnichannelAnalyticsService.ts
  39. 14
      packages/model-typings/src/models/ILivechatUnitModel.ts

@ -11,8 +11,12 @@ type SubscribedRooms = {
t: string;
};
export const closeOmnichannelConversations = async (user: IUser, subscribedRooms: SubscribedRooms[]): Promise<void> => {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
export const closeOmnichannelConversations = async (
user: IUser,
subscribedRooms: SubscribedRooms[],
executedBy?: string,
): Promise<void> => {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: executedBy });
const roomsInfo = LivechatRooms.findByIds(
subscribedRooms.map(({ rid }) => rid),
{},

@ -50,7 +50,12 @@ async function reactivateDirectConversations(userId: string) {
}
}
export async function setUserActiveStatus(userId: string, active: boolean, confirmRelinquish = false): Promise<boolean | undefined> {
export async function setUserActiveStatus(
userId: string,
active: boolean,
confirmRelinquish = false,
executedBy?: string,
): Promise<boolean | undefined> {
check(userId, String);
check(active, Boolean);
@ -105,7 +110,7 @@ export async function setUserActiveStatus(userId: string, active: boolean, confi
// We don't want one killing the other :)
await Promise.allSettled([
closeOmnichannelConversations(user, livechatSubscribedRooms),
closeOmnichannelConversations(user, livechatSubscribedRooms, executedBy),
relinquishRoomOwnerships(user._id, chatSubscribedRooms, false),
]);
}

@ -72,6 +72,7 @@ API.v1.addRoute(
queued,
units,
options: { offset, count, sort, fields },
callerId: this.userId,
}),
);
},

@ -24,9 +24,20 @@ async function findTriggers(): Promise<Pick<ILivechatTrigger, '_id' | 'actions'
}));
}
export type CheckUnitsFromUser = {
userId?: string;
businessUnit?: string;
};
export const checkUnitsFromUser = makeFunction(async (_params: CheckUnitsFromUser): Promise<void> => undefined);
async function findDepartments(
businessUnit?: string,
userId?: string,
): Promise<Pick<ILivechatDepartment, '_id' | 'name' | 'showOnRegistration' | 'showOnOfflineForm' | 'departmentsAllowedToForward'>[]> {
// TODO: check this function usage
await checkUnitsFromUser({ userId, businessUnit });
return LivechatDepartment.findEnabledWithAgentsAndBusinessUnit<
Pick<ILivechatDepartment, '_id' | 'name' | 'showOnRegistration' | 'showOnOfflineForm' | 'departmentsAllowedToForward'>
>(businessUnit, {
@ -63,7 +74,7 @@ export async function findRoom(token: string, rid?: string): Promise<IOmnichanne
return LivechatRooms.findOneByIdAndVisitorToken(rid, token, fields);
}
export async function findOpenRoom(token: string, departmentId?: string): Promise<IOmnichannelRoom | undefined> {
export async function findOpenRoom(token: string, departmentId?: string, callerId?: string): Promise<IOmnichannelRoom | undefined> {
const options = {
projection: {
departmentId: 1,
@ -73,7 +84,7 @@ export async function findOpenRoom(token: string, departmentId?: string): Promis
},
};
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: callerId });
const rooms = departmentId
? await LivechatRooms.findOpenByVisitorTokenAndDepartmentId(token, departmentId, options, extraQuery).toArray()
: await LivechatRooms.findOpenByVisitorToken(token, options, extraQuery).toArray();
@ -93,11 +104,13 @@ export function normalizeHttpHeaderData(headers: Headers = new Headers()): {
return { httpHeaders };
}
export async function settings({ businessUnit = '' }: { businessUnit?: string } = {}): Promise<Record<string, string | number | any>> {
export async function settings({ businessUnit = '', userId }: { businessUnit?: string; userId?: string } = {}): Promise<
Record<string, string | number | any>
> {
const [initSettings, triggers, departments, emojis] = await Promise.all([
getInitSettings(),
findTriggers(),
findDepartments(businessUnit),
findDepartments(businessUnit, userId),
EmojiCustom.find().toArray(),
]);
const sound = `${Meteor.absoluteUrl()}sounds/chime.mp3`;

@ -17,6 +17,7 @@ export async function findRooms({
queued,
units,
options: { offset, count, fields, sort },
callerId,
}: {
agents?: Array<string>;
roomName?: string;
@ -36,8 +37,9 @@ export async function findRooms({
queued?: string | boolean;
units?: Array<string>;
options: { offset: number; count: number; fields: Record<string, number>; sort: Record<string, number> };
callerId: string;
}): Promise<PaginatedResult<{ rooms: Array<IOmnichannelRoom> }>> {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, units);
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { unitsFilter: units, userId: callerId });
const { cursor, totalCount } = LivechatRooms.findRoomsWithCriteria({
agents,
roomName,

@ -62,7 +62,7 @@ export async function findChatHistory({
throw new Error('error-not-allowed');
}
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId });
const { cursor, totalCount } = LivechatRooms.findPaginatedByVisitorId(
visitorId,
{

@ -37,7 +37,7 @@ API.v1.addRoute(
{
async get() {
const { token } = this.urlParams;
const room = await findOpenRoom(token);
const room = await findOpenRoom(token, undefined, this.userId);
if (room) {
return API.v1.success();
}

@ -26,7 +26,7 @@ API.v1.addRoute(
token ? findGuestWithoutActivity(token) : null,
]);
const room = guest ? await findOpenRoom(guest.token) : undefined;
const room = guest ? await findOpenRoom(guest.token, undefined, this.userId) : undefined;
const agentPromise = room?.servedBy ? findAgent(room.servedBy._id) : null;
const extraInfoPromise = getExtraConfigInfo({ room });

@ -43,7 +43,7 @@ API.v1.addRoute(
}),
});
const contact = await registerContact(this.bodyParams);
const contact = await registerContact(this.bodyParams, this.userId);
return API.v1.success({ contact });
},

@ -256,7 +256,7 @@ API.v1.addRoute(
const visitor = await LivechatVisitors.getVisitorByToken(visitorToken, {});
let rid: string;
if (visitor) {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: this.userId });
const rooms = await LivechatRooms.findOpenByVisitorToken(visitorToken, {}, extraQuery).toArray();
if (rooms && rooms.length > 0) {
rid = rooms[0]._id;

@ -27,6 +27,7 @@ API.v1.addRoute(
utcOffset: user?.utcOffset || 0,
daterange: { from, to },
chartOptions: { name },
executedBy: this.userId,
}),
);
},
@ -58,6 +59,7 @@ API.v1.addRoute(
daterange: { from, to },
analyticsOptions: { name },
language,
executedBy: this.userId,
}),
);
},

@ -66,7 +66,7 @@ API.v1.addRoute(
});
}
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: this.userId });
// If it's updating an existing visitor, it must also update the roomInfo
const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray();
await Promise.all(
@ -169,7 +169,7 @@ API.v1.addRoute('livechat/visitor/:token', {
if (!visitor) {
throw new Meteor.Error('invalid-token');
}
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: this.userId });
const rooms = await LivechatRooms.findOpenByVisitorToken(
this.urlParams.token,
{
@ -210,7 +210,7 @@ API.v1.addRoute(
{ authRequired: true, permissionsRequired: ['view-livechat-manager'] },
{
async get() {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: this.userId });
const rooms = await LivechatRooms.findOpenByVisitorToken(
this.urlParams.token,
{

@ -53,6 +53,7 @@ const getProductivityMetricsAsync = async ({
departmentId,
utcOffset: user?.utcOffset,
language: user?.language || settings.get('Language') || 'en',
executedBy: user._id,
})) || [];
const averageWaitingTime = await findAllAverageWaitingTimeAsync({
start: new Date(start),
@ -111,6 +112,7 @@ const getAgentsProductivityMetricsAsync = async ({
departmentId,
utcOffset: user.utcOffset,
language: user.language || settings.get('Language') || 'en',
executedBy: user._id,
})) || [];
const totalOfServiceTime = averageOfServiceTime.departments.length;
@ -245,6 +247,7 @@ const getConversationsMetricsAsync = async ({
...(departmentId && departmentId !== 'undefined' && { departmentId }),
utcOffset: user.utcOffset,
language: user.language || settings.get('Language') || 'en',
executedBy: user._id,
})) || [];
const metrics = ['Total_conversations', 'Open_conversations', 'On_Hold_conversations', 'Total_messages'];
const visitorsCount = await LivechatVisitors.countVisitorsBetweenDate({

@ -25,15 +25,10 @@ type RegisterContactProps = {
};
};
export async function registerContact({
token,
name,
email = '',
phone,
username,
customFields = {},
contactManager,
}: RegisterContactProps): Promise<string> {
export async function registerContact(
{ token, name, email = '', phone, username, customFields = {}, contactManager }: RegisterContactProps,
userId: string,
): Promise<string> {
if (!token || typeof token !== 'string') {
throw new MeteorError('error-invalid-contact-data', 'Invalid visitor token');
}
@ -98,7 +93,7 @@ export async function registerContact({
await LivechatVisitors.updateOne({ _id: visitorId }, updateUser);
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId });
const rooms: IOmnichannelRoom[] = await LivechatRooms.findByVisitorId(visitorId, {}, extraQuery).toArray();
if (rooms?.length) {

@ -32,6 +32,6 @@ Meteor.methods<ServerMethods>({
return;
}
return OmnichannelAnalytics.getAnalyticsChartData({ ...options, utcOffset: user?.utcOffset });
return OmnichannelAnalytics.getAnalyticsChartData({ ...options, utcOffset: user?.utcOffset, executedBy: userId });
},
});

@ -29,7 +29,7 @@ Meteor.methods<ServerMethods>({
// These are not debug logs since we want to know when the action is performed
logger.info(`User ${Meteor.userId()} is removing all closed rooms`);
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: user });
const promises: Promise<void>[] = [];
await LivechatRooms.findClosedRooms(departmentIds, {}, extraQuery).forEach(({ _id }: IOmnichannelRoom) => {
promises.push(removeOmnichannelRoom(_id));

@ -3,6 +3,8 @@ import { LivechatUnitMonitors, LivechatUnit } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type { FindOptions } from 'mongodb';
import { getUnitsFromUser } from '../../methods/getUnitsFromUserRoles';
export async function findUnitsOfUser({
text,
userId,
@ -85,6 +87,7 @@ export async function findUnitMonitors({ unitId }: { unitId: string }): Promise<
return LivechatUnitMonitors.find({ unitId }).toArray();
}
export async function findUnitById({ unitId }: { unitId: string }): Promise<IOmnichannelBusinessUnit | null> {
return LivechatUnit.findOneById<IOmnichannelBusinessUnit>(unitId);
export async function findUnitById({ unitId, userId }: { unitId: string; userId: string }): Promise<IOmnichannelBusinessUnit | null> {
const unitsFromUser = await getUnitsFromUser(userId);
return LivechatUnit.findOneById(unitId, {}, { unitsFromUser });
}

@ -47,7 +47,7 @@ API.v1.addRoute(
checkDates(startDate, endDate);
const extraQuery = await restrictQuery();
const extraQuery = await restrictQuery({ userId: this.userId });
const result = await findAllConversationsBySourceCached({ start: startDate.toDate(), end: endDate.toDate(), extraQuery });
return API.v1.success(result);
@ -71,7 +71,7 @@ API.v1.addRoute(
const endDate = moment(end);
checkDates(startDate, endDate);
const extraQuery = await restrictQuery();
const extraQuery = await restrictQuery({ userId: this.userId });
const result = await findAllConversationsByStatusCached({ start: startDate.toDate(), end: endDate.toDate(), extraQuery });
return API.v1.success(result);
@ -96,7 +96,7 @@ API.v1.addRoute(
const endDate = moment(end);
checkDates(startDate, endDate);
const extraQuery = await restrictQuery();
const extraQuery = await restrictQuery({ userId: this.userId });
const result = await findAllConversationsByDepartmentCached({ start: startDate.toDate(), end: endDate.toDate(), sort, extraQuery });
return API.v1.success(result);
@ -121,7 +121,7 @@ API.v1.addRoute(
const endDate = moment(end);
checkDates(startDate, endDate);
const extraQuery = await restrictQuery();
const extraQuery = await restrictQuery({ userId: this.userId });
const result = await findAllConversationsByTagsCached({ start: startDate.toDate(), end: endDate.toDate(), sort, extraQuery });
return API.v1.success(result);
@ -146,7 +146,7 @@ API.v1.addRoute(
const endDate = moment(end);
checkDates(startDate, endDate);
const extraQuery = await restrictQuery();
const extraQuery = await restrictQuery({ userId: this.userId });
const result = await findAllConversationsByAgentsCached({ start: startDate.toDate(), end: endDate.toDate(), sort, extraQuery });
return API.v1.success(result);

@ -40,11 +40,15 @@ API.v1.addRoute(
async post() {
const { name, description, dueTimeInMinutes } = this.bodyParams;
const newSla = await LivechatEnterprise.saveSLA(null, {
name,
description,
dueTimeInMinutes,
});
const newSla = await LivechatEnterprise.saveSLA(
null,
{
name,
description,
dueTimeInMinutes,
},
this.userId,
);
return API.v1.success({ sla: newSla });
},
@ -79,7 +83,7 @@ API.v1.addRoute(
async delete() {
const { slaId } = this.urlParams;
await LivechatEnterprise.removeSLA(slaId);
await LivechatEnterprise.removeSLA(this.userId, slaId);
return API.v1.success();
},
@ -87,11 +91,15 @@ API.v1.addRoute(
const { name, description, dueTimeInMinutes } = this.bodyParams;
const { slaId } = this.urlParams;
const updatedSla = await LivechatEnterprise.saveSLA(slaId, {
name,
description,
dueTimeInMinutes,
});
const updatedSla = await LivechatEnterprise.saveSLA(
slaId,
{
name,
description,
dueTimeInMinutes,
},
this.userId,
);
return API.v1.success({ sla: updatedSla });
},

@ -72,7 +72,7 @@ API.v1.addRoute(
},
async post() {
const { unitData, unitMonitors, unitDepartments } = this.bodyParams;
return API.v1.success(await LivechatEnterprise.saveUnit(null, unitData, unitMonitors, unitDepartments));
return API.v1.success(await LivechatEnterprise.saveUnit(null, unitData, unitMonitors, unitDepartments, this.userId));
},
},
);
@ -85,6 +85,7 @@ API.v1.addRoute(
const { id } = this.urlParams;
const unit = await findUnitById({
unitId: id,
userId: this.userId,
});
return API.v1.success(unit);
@ -93,12 +94,12 @@ API.v1.addRoute(
const { unitData, unitMonitors, unitDepartments } = this.bodyParams;
const { id } = this.urlParams;
return API.v1.success(await LivechatEnterprise.saveUnit(id, unitData, unitMonitors, unitDepartments));
return API.v1.success(await LivechatEnterprise.saveUnit(id, unitData, unitMonitors, unitDepartments, this.userId));
},
async delete() {
const { id } = this.urlParams;
return API.v1.success((await LivechatEnterprise.removeUnit(id)).deletedCount);
return API.v1.success((await LivechatEnterprise.removeUnit(id, this.userId)).deletedCount);
},
},
);

@ -6,8 +6,17 @@ import { restrictQuery } from '../lib/restrictQuery';
callbacks.add(
'livechat.applyRoomRestrictions',
async (originalQuery: FilterOperators<IOmnichannelRoom> = {}, unitsFilter?: string[]) => {
return restrictQuery(originalQuery, unitsFilter);
async (
originalQuery: FilterOperators<IOmnichannelRoom> = {},
{
unitsFilter,
userId,
}: {
unitsFilter?: string[];
userId?: string;
} = {},
) => {
return restrictQuery({ originalQuery, unitsFilter, userId });
},
callbacks.priority.HIGH,
'livechat-apply-room-restrictions',

@ -1,4 +1,4 @@
import type { ILivechatDepartment, IOmnichannelBusinessUnit } from '@rocket.chat/core-typings';
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatUnit } from '@rocket.chat/models';
import { hasAnyRoleAsync } from '../../../../../app/authorization/server/functions/hasRole';
@ -26,9 +26,15 @@ export const manageDepartmentUnit = async ({ userId, departmentId, unitId }: { u
}
if (unitId) {
const unit = await LivechatUnit.findOneById<Pick<IOmnichannelBusinessUnit, '_id' | 'ancestors'>>(unitId, {
projection: { ancestors: 1 },
});
const unit = await LivechatUnit.findOneById(
unitId,
{
projection: { ancestors: 1 },
},
{
unitsFromUser: accessibleUnits,
},
);
if (!unit) {
return;

@ -35,6 +35,7 @@ await License.onLicense('livechat-enterprise', async () => {
await import('./startup');
const { createPermissions } = await import('./permissions');
const { createSettings } = await import('./settings');
await import('./lib/unit');
Meteor.startup(() => {
void createSettings();

@ -304,14 +304,17 @@ export const updateQueueInactivityTimeout = async () => {
});
};
export const updateSLAInquiries = async (sla?: Pick<IOmnichannelServiceLevelAgreements, '_id' | 'dueTimeInMinutes'>) => {
export const updateSLAInquiries = async (
executedBy: string,
sla?: Pick<IOmnichannelServiceLevelAgreements, '_id' | 'dueTimeInMinutes'>,
) => {
if (!sla) {
return;
}
const { _id: slaId } = sla;
const promises: Promise<void>[] = [];
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: executedBy });
await LivechatRooms.findOpenBySlaId(slaId, {}, extraQuery).forEach((room) => {
promises.push(updateInquiryQueueSla(room._id, sla));
});

@ -8,6 +8,7 @@ import { removeSLAFromRooms } from './SlaHelper';
import { callbacks } from '../../../../../lib/callbacks';
import { addUserRolesAsync } from '../../../../../server/lib/roles/addUserRoles';
import { removeUserFromRolesAsync } from '../../../../../server/lib/roles/removeUserFromRoles';
import { getUnitsFromUser } from '../methods/getUnitsFromUserRoles';
export const LivechatEnterprise = {
async addMonitor(username: string) {
@ -49,8 +50,12 @@ export const LivechatEnterprise = {
return true;
},
async removeUnit(_id: string) {
const result = await LivechatUnit.removeById(_id);
async removeUnit(_id: string, userId: string) {
check(_id, String);
const unitsFromUser = await getUnitsFromUser(userId);
const result = await LivechatUnit.removeByIdAndUnit(_id, unitsFromUser);
if (!result.deletedCount) {
throw new Meteor.Error('unit-not-found', 'Unit not found', { method: 'livechat:removeUnit' });
}
@ -63,6 +68,7 @@ export const LivechatEnterprise = {
unitData: Omit<IOmnichannelBusinessUnit, '_id'>,
unitMonitors: { monitorId: string; username: string },
unitDepartments: { departmentId: string }[],
userId: string,
) {
check(_id, Match.Maybe(String));
@ -90,9 +96,14 @@ export const LivechatEnterprise = {
let ancestors: string[] = [];
if (_id) {
const unit = await LivechatUnit.findOneById<Pick<IOmnichannelBusinessUnit, '_id' | 'ancestors'>>(_id, {
projection: { _id: 1, ancestors: 1 },
});
const unitsFromUser = await getUnitsFromUser(userId);
const unit = await LivechatUnit.findOneById<Pick<IOmnichannelBusinessUnit, '_id' | 'ancestors'>>(
_id,
{
projection: { _id: 1, ancestors: 1 },
},
{ unitsFromUser },
);
if (!unit) {
throw new Meteor.Error('error-unit-not-found', 'Unit not found', {
method: 'livechat:saveUnit',
@ -131,7 +142,11 @@ export const LivechatEnterprise = {
return LivechatTag.createOrUpdateTag(_id, tagData, tagDepartments);
},
async saveSLA(_id: string | null, slaData: Pick<IOmnichannelServiceLevelAgreements, 'name' | 'description' | 'dueTimeInMinutes'>) {
async saveSLA(
_id: string | null,
slaData: Pick<IOmnichannelServiceLevelAgreements, 'name' | 'description' | 'dueTimeInMinutes'>,
executedBy: string,
) {
const oldSLA =
_id &&
(await OmnichannelServiceLevelAgreements.findOneById<Pick<IOmnichannelServiceLevelAgreements, 'dueTimeInMinutes'>>(_id, {
@ -151,18 +166,18 @@ export const LivechatEnterprise = {
const { dueTimeInMinutes } = sla;
if (oldDueTimeInMinutes !== dueTimeInMinutes) {
await updateSLAInquiries(sla);
await updateSLAInquiries(executedBy, sla);
}
return sla;
},
async removeSLA(_id: string) {
async removeSLA(executedBy: string, _id: string) {
const removedResult = await OmnichannelServiceLevelAgreements.removeById(_id);
if (!removedResult || removedResult.deletedCount !== 1) {
throw new Error(`SLA with id ${_id} not found`);
}
await removeSLAFromRooms(_id);
await removeSLAFromRooms(_id, executedBy);
},
};

@ -4,8 +4,8 @@ import { LivechatInquiry, LivechatRooms } from '@rocket.chat/models';
import { callbacks } from '../../../../../lib/callbacks';
export const removeSLAFromRooms = async (slaId: string) => {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
export const removeSLAFromRooms = async (slaId: string, userId: string) => {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId });
const openRooms = await LivechatRooms.findOpenBySlaId(slaId, { projection: { _id: 1 } }, extraQuery).toArray();
if (openRooms.length) {
const openRoomIds: string[] = openRooms.map(({ _id }) => _id);

@ -3,12 +3,20 @@ import { LivechatDepartment } from '@rocket.chat/models';
import type { FilterOperators } from 'mongodb';
import { cbLogger } from './logger';
import { getUnitsFromUser } from './units';
import { getUnitsFromUser } from '../methods/getUnitsFromUserRoles';
export const restrictQuery = async (originalQuery: FilterOperators<IOmnichannelRoom> = {}, unitsFilter?: string[]) => {
export const restrictQuery = async ({
originalQuery = {},
unitsFilter,
userId,
}: {
originalQuery?: FilterOperators<IOmnichannelRoom>;
unitsFilter?: string[];
userId?: string;
}) => {
const query = { ...originalQuery };
let userUnits = await getUnitsFromUser();
let userUnits = await getUnitsFromUser(userId);
if (!Array.isArray(userUnits)) {
if (Array.isArray(unitsFilter) && unitsFilter.length) {
return { ...query, departmentAncestors: { $in: unitsFilter } };

@ -0,0 +1,16 @@
import { LivechatUnit } from '@rocket.chat/models';
import type { CheckUnitsFromUser } from '../../../../../app/livechat/server/api/lib/livechat';
import { checkUnitsFromUser } from '../../../../../app/livechat/server/api/lib/livechat';
import { getUnitsFromUser } from '../methods/getUnitsFromUserRoles';
checkUnitsFromUser.patch(async (_next, { businessUnit, userId }: CheckUnitsFromUser) => {
if (!businessUnit) {
return;
}
const unitsFromUser = await getUnitsFromUser(userId);
const unit = await LivechatUnit.findOneById(businessUnit, { projection: { _id: 1 } }, { unitsFromUser });
if (!unit) {
throw new Meteor.Error('error-unit-not-found', `Error! No Active Business Unit found with id: ${businessUnit}`);
}
});

@ -1,19 +0,0 @@
import { LivechatUnit } from '@rocket.chat/models';
import mem from 'mem';
import { Meteor } from 'meteor/meteor';
async function hasUnits(): Promise<boolean> {
// @ts-expect-error - this prop is injected dynamically on ee license
return (await LivechatUnit.countUnits({ type: 'u' })) > 0;
}
// Units should't change really often, so we can cache the result
const memoizedHasUnits = mem(hasUnits, { maxAge: process.env.TEST_MODE ? 1 : 10000 });
export async function getUnitsFromUser(): Promise<string[] | undefined> {
if (!(await memoizedHasUnits())) {
return;
}
return Meteor.callAsync('livechat:getUnitsFromUser');
}

@ -18,18 +18,34 @@ async function getDepartmentsFromUserRoles(user: string): Promise<string[]> {
const memoizedGetUnitFromUserRoles = mem(getUnitsFromUserRoles, { maxAge: process.env.TEST_MODE ? 1 : 10000 });
const memoizedGetDepartmentsFromUserRoles = mem(getDepartmentsFromUserRoles, { maxAge: process.env.TEST_MODE ? 1 : 10000 });
export const getUnitsFromUser = async (user: string): Promise<string[] | undefined> => {
async function hasUnits(): Promise<boolean> {
// @ts-expect-error - this prop is injected dynamically on ee license
return (await LivechatUnit.countUnits({ type: 'u' })) > 0;
}
// Units should't change really often, so we can cache the result
const memoizedHasUnits = mem(hasUnits, { maxAge: process.env.TEST_MODE ? 1 : 10000 });
export const getUnitsFromUser = async (userId?: string): Promise<string[] | undefined> => {
if (!userId) {
return;
}
if (!(await memoizedHasUnits())) {
return;
}
// TODO: we can combine these 2 calls into one single query
if (!user || (await hasAnyRoleAsync(user, ['admin', 'livechat-manager']))) {
if (await hasAnyRoleAsync(userId, ['admin', 'livechat-manager'])) {
return;
}
if (!(await hasAnyRoleAsync(user, ['livechat-monitor']))) {
if (!(await hasAnyRoleAsync(userId, ['livechat-monitor']))) {
return;
}
const unitsAndDepartments = [...(await memoizedGetUnitFromUserRoles(user)), ...(await memoizedGetDepartmentsFromUserRoles(user))];
logger.debug({ msg: 'Calculating units for monitor', user, unitsAndDepartments });
const unitsAndDepartments = [...(await memoizedGetUnitFromUserRoles(userId)), ...(await memoizedGetDepartmentsFromUserRoles(userId))];
logger.debug({ msg: 'Calculating units for monitor', user: userId, unitsAndDepartments });
return unitsAndDepartments;
};
@ -44,10 +60,10 @@ declare module '@rocket.chat/ddp-client' {
Meteor.methods<ServerMethods>({
async 'livechat:getUnitsFromUser'(): Promise<string[] | undefined> {
methodDeprecationLogger.method('livechat:getUnitsFromUser', '8.0.0');
const user = Meteor.userId();
if (!user) {
const userId = Meteor.userId();
if (!userId) {
return;
}
return getUnitsFromUser(user);
return getUnitsFromUser(userId);
},
});

@ -20,6 +20,6 @@ Meteor.methods<ServerMethods>({
}
check(id, String);
return (await LivechatEnterprise.removeUnit(id)).deletedCount > 0;
return (await LivechatEnterprise.removeUnit(id, uid)).deletedCount > 0;
},
});

@ -19,6 +19,6 @@ Meteor.methods<ServerMethods>({
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveUnit' });
}
return LivechatEnterprise.saveUnit(_id, unitData, unitMonitors, unitDepartments);
return LivechatEnterprise.saveUnit(_id, unitData, unitMonitors, unitDepartments, uid);
},
});

@ -27,12 +27,14 @@ const getRoomInfoByAuditParams = async ({
users: usernames,
visitor,
agent,
userId,
}: {
type: string;
roomId: IRoom['_id'];
users: NonNullable<IUser['username']>[];
visitor: ILivechatVisitor['_id'];
agent: ILivechatAgent['_id'];
userId: string;
}) => {
if (rid) {
return getValue(await Rooms.findOne({ _id: rid }));
@ -44,7 +46,7 @@ const getRoomInfoByAuditParams = async ({
if (type === 'l') {
console.warn('Deprecation Warning! This method will be removed in the next version (4.0.0)');
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId });
const rooms: IRoom[] = await LivechatRooms.findByVisitorIdAndAgentId(
visitor,
agent,
@ -161,7 +163,7 @@ Meteor.methods<ServerMethods>({
const usersId = await getUsersIdFromUserName(usernames);
query['u._id'] = { $in: usersId };
} else {
const roomInfo = await getRoomInfoByAuditParams({ type, roomId: rid, users: usernames, visitor, agent });
const roomInfo = await getRoomInfoByAuditParams({ type, roomId: rid, users: usernames, visitor, agent, userId: user._id });
if (!roomInfo) {
throw new Meteor.Error('Room doesn`t exist');
}

@ -3,15 +3,12 @@ import type { FindPaginated, ILivechatUnitModel } from '@rocket.chat/model-typin
import { LivechatUnitMonitors, LivechatDepartment, LivechatRooms, BaseRaw } from '@rocket.chat/models';
import type { FindOptions, Filter, FindCursor, Db, FilterOperators, UpdateResult, DeleteResult, Document, UpdateFilter } from 'mongodb';
import { getUnitsFromUser } from '../../../app/livechat-enterprise/server/lib/units';
const addQueryRestrictions = async (originalQuery: Filter<IOmnichannelBusinessUnit> = {}) => {
const addQueryRestrictions = async (originalQuery: Filter<IOmnichannelBusinessUnit> = {}, unitsFromUser?: string[]) => {
const query: FilterOperators<IOmnichannelBusinessUnit> = { ...originalQuery, type: 'u' };
const units = await getUnitsFromUser();
if (Array.isArray(units)) {
if (Array.isArray(unitsFromUser)) {
const expressions = query.$and || [];
const condition = { $or: [{ ancestors: { $in: units } }, { _id: { $in: units } }] };
const condition = { $or: [{ ancestors: { $in: unitsFromUser } }, { _id: { $in: unitsFromUser } }] };
query.$and = [condition, ...expressions];
}
@ -32,21 +29,24 @@ export class LivechatUnitRaw extends BaseRaw<IOmnichannelBusinessUnit> implement
}
// @ts-expect-error - Overriding base types :)
async find(
async findOne<P extends Document = IOmnichannelBusinessUnit>(
originalQuery: Filter<IOmnichannelBusinessUnit>,
options: FindOptions<IOmnichannelBusinessUnit>,
): Promise<FindCursor<IOmnichannelBusinessUnit>> {
const query = await addQueryRestrictions(originalQuery);
return this.col.find(query, options) as FindCursor<IOmnichannelBusinessUnit>;
extra?: Record<string, any>,
): Promise<P | null> {
const query = await addQueryRestrictions(originalQuery, extra?.unitsFromUser);
return this.col.findOne<P>(query, options);
}
// @ts-expect-error - Overriding base types :)
async findOne(
originalQuery: Filter<IOmnichannelBusinessUnit>,
async findOneById<P extends Document = IOmnichannelBusinessUnit>(
_id: IOmnichannelBusinessUnit['_id'],
options: FindOptions<IOmnichannelBusinessUnit>,
): Promise<IOmnichannelBusinessUnit | null> {
const query = await addQueryRestrictions(originalQuery);
return this.col.findOne(query, options);
extra?: Record<string, any>,
): Promise<P | null> {
if (options) {
return this.findOne<P>({ _id }, options, extra);
}
return this.findOne<P>({ _id }, {}, extra);
}
remove(query: Filter<IOmnichannelBusinessUnit>): Promise<DeleteResult> {
@ -153,6 +153,19 @@ export class LivechatUnitRaw extends BaseRaw<IOmnichannelBusinessUnit> implement
return this.deleteOne(query);
}
async removeByIdAndUnit(_id: string, unitsFromUser?: string[]): Promise<DeleteResult> {
const originalQuery = { _id };
const query = await addQueryRestrictions(originalQuery, unitsFromUser);
const result = await this.deleteOne(query);
if (result.deletedCount > 0) {
await LivechatUnitMonitors.removeByUnitId(_id);
await this.removeParentAndAncestorById(_id);
await LivechatRooms.removeUnitAssociationFromRooms(_id);
}
return result;
}
findOneByIdOrName(_idOrName: string, options: FindOptions<IOmnichannelBusinessUnit>): Promise<IOmnichannelBusinessUnit | null> {
const query = {
$or: [

@ -170,7 +170,13 @@ type ChainedCallbackSignatures = {
query: FilterOperators<ILivechatDepartmentRecord>,
params: { userId: IUser['_id'] },
) => FilterOperators<ILivechatDepartmentRecord>;
'livechat.applyRoomRestrictions': (query: FilterOperators<IOmnichannelRoom>, unitsFilter?: string[]) => FilterOperators<IOmnichannelRoom>;
'livechat.applyRoomRestrictions': (
query: FilterOperators<IOmnichannelRoom>,
params?: {
unitsFilter?: string[];
userId?: string;
},
) => FilterOperators<IOmnichannelRoom>;
'livechat.onMaxNumberSimultaneousChatsReached': (inquiry: ILivechatInquiryRecord) => ILivechatInquiryRecord;
'on-business-hour-start': (params: { BusinessHourBehaviorClass: { new (): IBusinessHourBehavior } }) => {
BusinessHourBehaviorClass: { new (): IBusinessHourBehavior };

@ -27,7 +27,7 @@ export const executeSetUserActiveStatus = async (
});
}
await setUserActiveStatus(userId, active, confirmRelinquish);
await setUserActiveStatus(userId, active, confirmRelinquish, fromUserId);
return true;
};

@ -38,7 +38,7 @@ export class OmnichannelAnalyticsService extends ServiceClassInternal implements
}
async getAgentOverviewData(options: AgentOverviewDataOptions) {
const { departmentId, utcOffset, daterange: { from: fDate, to: tDate } = {}, chartOptions: { name } = {} } = options;
const { departmentId, utcOffset, daterange: { from: fDate, to: tDate } = {}, chartOptions: { name } = {}, executedBy } = options;
const timezone = getTimezone({ utcOffset });
const from = moment
.tz(fDate || '', 'YYYY-MM-DD', timezone)
@ -59,7 +59,7 @@ export class OmnichannelAnalyticsService extends ServiceClassInternal implements
return;
}
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: executedBy });
return this.agentOverview.callAction(name, from, to, departmentId, extraQuery);
}
@ -69,6 +69,7 @@ export class OmnichannelAnalyticsService extends ServiceClassInternal implements
departmentId,
daterange: { from: fDate, to: tDate } = {},
chartOptions: { name: chartLabel },
executedBy,
} = options;
// Check if function exists, prevent server error in case property altered
@ -103,7 +104,7 @@ export class OmnichannelAnalyticsService extends ServiceClassInternal implements
dataPoints: [],
};
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: executedBy });
if (isSameDay) {
// data for single day
const m = moment(from);
@ -139,7 +140,14 @@ export class OmnichannelAnalyticsService extends ServiceClassInternal implements
}
async getAnalyticsOverviewData(options: AnalyticsOverviewDataOptions) {
const { departmentId, utcOffset = 0, language, daterange: { from: fDate, to: tDate } = {}, analyticsOptions: { name } = {} } = options;
const {
departmentId,
utcOffset = 0,
language,
daterange: { from: fDate, to: tDate } = {},
analyticsOptions: { name } = {},
executedBy,
} = options;
const timezone = getTimezone({ utcOffset });
const from = moment
.tz(fDate || '', 'YYYY-MM-DD', timezone)
@ -162,7 +170,7 @@ export class OmnichannelAnalyticsService extends ServiceClassInternal implements
const t = i18n.getFixedT(language);
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: executedBy });
return this.overview.callAction(name, from, to, departmentId, timezone, t, extraQuery);
}
}

@ -15,6 +15,7 @@ export type AgentOverviewDataOptions = {
chartOptions: {
name: string;
};
executedBy: string;
};
export type ChartDataOptions = {
@ -27,6 +28,7 @@ export type ChartDataOptions = {
chartOptions: {
name: string;
};
executedBy: string;
};
export type AnalyticsOverviewDataOptions = {
@ -40,6 +42,7 @@ export type AnalyticsOverviewDataOptions = {
analyticsOptions: {
name: string;
};
executedBy: string;
};
export type ChartDataResult = {

@ -10,19 +10,16 @@ export interface ILivechatUnitModel extends IBaseModel<IOmnichannelBusinessUnit>
query: Filter<IOmnichannelBusinessUnit>,
options?: FindOptions<IOmnichannelBusinessUnit>,
): FindPaginated<FindCursor<IOmnichannelBusinessUnit>>;
find(
originalQuery: Filter<IOmnichannelBusinessUnit>,
options: FindOptions<IOmnichannelBusinessUnit>,
): FindCursor<IOmnichannelBusinessUnit>;
findOne(
originalQuery: Filter<IOmnichannelBusinessUnit>,
options: FindOptions<IOmnichannelBusinessUnit>,
extra?: Record<string, any>,
): Promise<IOmnichannelBusinessUnit | null>;
update(
originalQuery: Filter<IOmnichannelBusinessUnit>,
update: Filter<IOmnichannelBusinessUnit>,
findOneById<P extends Document = IOmnichannelBusinessUnit>(
_id: IOmnichannelBusinessUnit['_id'],
options: FindOptions<IOmnichannelBusinessUnit>,
): Promise<UpdateResult>;
extra?: Record<string, any>,
): Promise<P | null>;
remove(query: Filter<IOmnichannelBusinessUnit>): Promise<DeleteResult>;
createOrUpdateUnit(
_id: string | null,
@ -35,6 +32,7 @@ export interface ILivechatUnitModel extends IBaseModel<IOmnichannelBusinessUnit>
incrementDepartmentsCount(_id: string): Promise<UpdateResult | Document>;
decrementDepartmentsCount(_id: string): Promise<UpdateResult | Document>;
removeById(_id: string): Promise<DeleteResult>;
removeByIdAndUnit(_id: string, unitsFromUser?: string[]): Promise<DeleteResult>;
findOneByIdOrName(_idOrName: string, options: FindOptions<IOmnichannelBusinessUnit>): Promise<IOmnichannelBusinessUnit | null>;
findByMonitorId(monitorId: string): Promise<string[]>;
findMonitoredDepartmentsByMonitorId(monitorId: string, includeDisabled: boolean): Promise<ILivechatDepartment[]>;

Loading…
Cancel
Save