Skip to content

Commit

Permalink
Merge branch 'develop' into replace-oauth2-server-lib
Browse files Browse the repository at this point in the history
  • Loading branch information
julio-rocketchat authored Feb 3, 2025
2 parents 63b9220 + 3fda478 commit 7cbb029
Show file tree
Hide file tree
Showing 47 changed files with 699 additions and 522 deletions.
5 changes: 5 additions & 0 deletions .changeset/dry-jeans-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes an issue that allowed departments to be removed via API even with setting `Omnichannel_enable_department_removal` disabled
5 changes: 5 additions & 0 deletions .changeset/funny-ears-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes a rerender on each sidebar item click
8 changes: 8 additions & 0 deletions .changeset/slow-flies-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/core-typings": minor
"@rocket.chat/model-typings": minor
---

Replaces Livechat Visitors by Contacts on workspaces' MAC count.
This allows a more accurate and potentially smaller MAC count in case Contact Identification is enabled, since multiple visitors may be associated to the same contact.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LivechatRooms, Statistics, Users } from '@rocket.chat/models';
import { LivechatContacts, Statistics, Users } from '@rocket.chat/models';
import moment from 'moment';

import { settings } from '../../../settings/server';
Expand Down Expand Up @@ -69,7 +69,7 @@ export async function buildWorkspaceRegistrationData<T extends string | undefine
const workspaceType = settings.get<string>('Server_Type');

const seats = await Users.getActiveLocalUserCount();
const [macs] = await LivechatRooms.getMACStatisticsForPeriod(moment.utc().format('YYYY-MM'));
const MAC = await LivechatContacts.countContactsOnPeriod(moment.utc().format('YYYY-MM'));

const license = settings.get<string>('Enterprise_License');

Expand Down Expand Up @@ -102,7 +102,7 @@ export async function buildWorkspaceRegistrationData<T extends string | undefine
setupComplete: setupWizardState === 'completed',
connectionDisable: false,
npsEnabled,
MAC: macs?.contactsCount ?? 0,
MAC,
// activeContactsBillingMonth: stats.omnichannelContactsBySource.contactsCount,
// activeContactsYesterday: stats.uniqueContactsOfYesterday.contactsCount,
statsToken: stats.statsToken,
Expand Down
1 change: 0 additions & 1 deletion apps/meteor/app/livechat-enterprise/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { hasLicense } from '../../license/client';
import './startup';

void hasLicense('livechat-enterprise').then((enabled) => {
if (!enabled) {
Expand Down
22 changes: 0 additions & 22 deletions apps/meteor/app/livechat-enterprise/client/startup.ts

This file was deleted.

24 changes: 24 additions & 0 deletions apps/meteor/app/livechat-enterprise/hooks/useLivechatEnterprise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useSetting } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';

import { useHasLicenseModule } from '../../../client/hooks/useHasLicenseModule';
import { businessHourManager } from '../../livechat/client/views/app/business-hours/BusinessHours';
import type { IBusinessHourBehavior } from '../../livechat/client/views/app/business-hours/IBusinessHourBehavior';
import { SingleBusinessHourBehavior } from '../../livechat/client/views/app/business-hours/Single';
import { MultipleBusinessHoursBehavior } from '../client/views/business-hours/Multiple';

const businessHours: Record<string, IBusinessHourBehavior> = {
multiple: new MultipleBusinessHoursBehavior(),
single: new SingleBusinessHourBehavior(),
};

export const useLivechatEnterprise = () => {
const businessHourType = useSetting('Livechat_business_hour_type') as string;
const hasLicense = useHasLicenseModule('livechat-enterprise');

useEffect(() => {
if (businessHourType && hasLicense) {
businessHourManager.registerBusinessHourBehavior(businessHours[businessHourType.toLowerCase()]);
}
}, [businessHourType, hasLicense]);
};
7 changes: 7 additions & 0 deletions apps/meteor/app/livechat/imports/server/rest/departments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Match, check } from 'meteor/check';
import { API } from '../../../../api/server';
import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems';
import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission';
import { settings } from '../../../../settings/server';
import {
findDepartments,
findDepartmentById,
Expand Down Expand Up @@ -164,6 +165,12 @@ API.v1.addRoute(
_id: String,
});

const isRemoveEnabled = settings.get<boolean>('Omnichannel_enable_department_removal');

if (!isRemoveEnabled) {
return API.v1.failure('error-department-removal-disabled');
}

await removeDepartment(this.urlParams._id);

return API.v1.success();
Expand Down
8 changes: 4 additions & 4 deletions apps/meteor/app/livechat/server/hooks/markRoomResponded.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IOmnichannelRoom, IMessage } from '@rocket.chat/core-typings';
import { isEditedMessage, isMessageFromVisitor, isSystemMessage } from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/models';
import { LivechatRooms, LivechatVisitors, LivechatInquiry } from '@rocket.chat/models';
import { LivechatRooms, LivechatContacts, LivechatInquiry } from '@rocket.chat/models';
import moment from 'moment';

import { callbacks } from '../../../../lib/callbacks';
Expand All @@ -17,11 +17,11 @@ export async function markRoomResponded(
}

const monthYear = moment().format('YYYY-MM');
const isVisitorActive = await LivechatVisitors.isVisitorActiveOnPeriod(room.v._id, monthYear);
const isContactActive = await LivechatContacts.isContactActiveOnPeriod({ visitorId: room.v._id, source: room.source }, monthYear);

// Case: agent answers & visitor is not active, we mark visitor as active
if (!isVisitorActive) {
await LivechatVisitors.markVisitorActiveForPeriod(room.v._id, monthYear);
if (!isContactActive) {
await LivechatContacts.markContactActiveForPeriod({ visitorId: room.v._id, source: room.source }, monthYear);
}

if (!room.v?.activity?.includes(monthYear)) {
Expand Down
23 changes: 13 additions & 10 deletions apps/meteor/app/livechat/server/lib/Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,6 @@ export const prepareLivechatRoom = async (
const extraRoomInfo = await callbacks.run('livechat.beforeRoom', roomInfo, extraData);
const { _id, username, token, department: departmentId, status = 'online' } = guest;
const newRoomAt = new Date();

const { activity } = guest;
logger.debug({
msg: `Creating livechat room for visitor ${_id}`,
visitor: { _id, username, departmentId, status, activity },
});

const source = extraRoomInfo.source || roomInfo.source;

if (settings.get<string>('Livechat_Require_Contact_Verification') === 'always') {
Expand All @@ -103,14 +96,20 @@ export const prepareLivechatRoom = async (
const contactId = await migrateVisitorIfMissingContact(_id, source);
const contact =
contactId &&
(await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'name' | 'channels'>>(contactId, {
projection: { name: 1, channels: 1 },
(await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'name' | 'channels' | 'activity'>>(contactId, {
projection: { name: 1, channels: 1, activity: 1 },
}));
if (!contact) {
throw new Error('error-invalid-contact');
}
const verified = Boolean(contact.channels.some((channel) => isVerifiedChannelInSource(channel, _id, source)));

const activity = guest.activity || contact.activity;
logger.debug({
msg: `Creating livechat room for visitor ${_id}`,
visitor: { _id, username, departmentId, status, activity },
});

// TODO: Solve `u` missing issue
return {
_id: rid,
Expand Down Expand Up @@ -199,7 +198,11 @@ export const createLivechatInquiry = async ({

const extraInquiryInfo = await callbacks.run('livechat.beforeInquiry', extraData);

const { _id, username, token, department, status = UserStatus.ONLINE, activity } = guest;
const { _id, username, token, department, status = UserStatus.ONLINE } = guest;
const inquirySource = extraData?.source || { type: OmnichannelSourceType.OTHER };
const activity =
guest.activity ||
(await LivechatContacts.findOneByVisitor({ visitorId: guest._id, source: inquirySource }, { projection: { activity: 1 } }))?.activity;

const ts = new Date();

Expand Down
18 changes: 17 additions & 1 deletion apps/meteor/app/livechat/server/lib/contacts/ContactMerger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type ContactFields = {
username: string;
manager: ManagerValue;
channel: ILivechatContactChannel;
activity: string[];
};

type CustomFieldAndValue = { type: `customFields.${string}`; value: string };
Expand All @@ -29,6 +30,7 @@ export type FieldAndValue =
| { type: keyof Omit<ContactFields, 'manager' | 'channel'>; value: string }
| { type: 'manager'; value: ManagerValue }
| { type: 'channel'; value: ILivechatContactChannel }
| { type: 'activity'; value: string[] }
| CustomFieldAndValue;

type ConflictHandlingMode = 'conflict' | 'overwrite' | 'ignore';
Expand Down Expand Up @@ -118,7 +120,7 @@ export class ContactMerger {
}

static getAllFieldsFromContact(contact: ILivechatContact): FieldAndValue[] {
const { customFields = {}, name, contactManager } = contact;
const { customFields = {}, name, contactManager, activity } = contact;

const fields = new Set<FieldAndValue>();

Expand All @@ -134,6 +136,10 @@ export class ContactMerger {
fields.add({ type: 'manager', value: { id: contactManager } });
}

if (activity) {
fields.add({ type: 'activity', value: activity });
}

Object.keys(customFields).forEach((key) =>
fields.add({ type: `customFields.${key}`, value: customFields[key] } as CustomFieldAndValue),
);
Expand Down Expand Up @@ -222,6 +228,7 @@ export class ContactMerger {
const newPhones = ContactMerger.getFieldValuesByType(newFields, 'phone');
const newEmails = ContactMerger.getFieldValuesByType(newFields, 'email');
const newChannels = ContactMerger.getFieldValuesByType(newFields, 'channel');
const newActivities = ContactMerger.getFieldValuesByType(newFields, 'activity');
const newNamesOnly = ContactMerger.getFieldValuesByType(newFields, 'name');
const newCustomFields = newFields.filter(({ type }) => type.startsWith('customFields.')) as CustomFieldAndValue[];
// Usernames are ignored unless the contact has no other name
Expand Down Expand Up @@ -254,6 +261,15 @@ export class ContactMerger {
}
}

if (newActivities.length) {
const newActivity = newActivities.shift();
if (newActivity) {
const distinctActivities = new Set([...newActivity, ...(contact.activity || [])]);
const latestActivities = Array.from(distinctActivities).sort().slice(-12);
dataToSet.activity = latestActivities;
}
}

const customFieldsPerName = new Map<string, CustomFieldAndValue[]>();
for (const customField of newCustomFields) {
if (!customFieldsPerName.has(customField.type)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const modelsMock = {
upsertContact: sinon.stub(),
updateContact: sinon.stub(),
findContactMatchingVisitor: sinon.stub(),
findOneByVisitorId: sinon.stub(),
},
'LivechatRooms': {
findNewestByVisitorIdOrToken: sinon.stub(),
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/providers/LayoutProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const LayoutProvider = ({ children }: LayoutProviderProps) => {
showTopNavbarEmbeddedLayout,
sidebar: {
isCollapsed,
toggle: () => setIsCollapsed((isCollapsed) => !isCollapsed),
toggle: isMobile ? () => setIsCollapsed((isCollapsed) => !isCollapsed) : () => undefined,
collapse: () => setIsCollapsed(true),
expand: () => setIsCollapsed(false),
close: () => (isEmbedded ? setIsCollapsed(true) : router.navigate('/home')),
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/client/views/root/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useEscapeKeyStroke } from './hooks/useEscapeKeyStroke';
import { useGoogleTagManager } from './hooks/useGoogleTagManager';
import { useMessageLinkClicks } from './hooks/useMessageLinkClicks';
import { useAnalytics } from '../../../app/analytics/client/loadScript';
import { useLivechatEnterprise } from '../../../app/livechat-enterprise/hooks/useLivechatEnterprise';
import { useNextcloud } from '../../../app/nextcloud/client/useNextcloud';
import { useAnalyticsEventTracking } from '../../hooks/useAnalyticsEventTracking';
import { useLoadRoomForAllowedAnonymousRead } from '../../hooks/useLoadRoomForAllowedAnonymousRead';
Expand All @@ -29,6 +30,7 @@ const AppLayout = () => {
useLoadRoomForAllowedAnonymousRead();
useNotifyUser();

useLivechatEnterprise();
useNextcloud();

const layout = useSyncExternalStore(appLayout.subscribe, appLayout.getSnapshot);
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/ee/app/license/server/startup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { api } from '@rocket.chat/core-services';
import type { LicenseLimitKind } from '@rocket.chat/core-typings';
import { applyLicense, applyLicenseOrRemove, License } from '@rocket.chat/license';
import { Subscriptions, Users, Settings, LivechatVisitors } from '@rocket.chat/models';
import { Subscriptions, Users, Settings, LivechatContacts } from '@rocket.chat/models';
import { wrapExceptions } from '@rocket.chat/tools';
import moment from 'moment';

Expand Down Expand Up @@ -110,7 +110,7 @@ export const startLicense = async () => {
License.setLicenseLimitCounter('roomsPerGuest', async (context) => (context?.userId ? Subscriptions.countByUserId(context.userId) : 0));
License.setLicenseLimitCounter('privateApps', () => getAppCount('private'));
License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace'));
License.setLicenseLimitCounter('monthlyActiveContacts', () => LivechatVisitors.countVisitorsOnPeriod(moment.utc().format('YYYY-MM')));
License.setLicenseLimitCounter('monthlyActiveContacts', () => LivechatContacts.countContactsOnPeriod(moment.utc().format('YYYY-MM')));

return new Promise<void>((resolve) => {
// When settings are loaded, apply the current license if there is one.
Expand Down
12 changes: 6 additions & 6 deletions apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@
"@rocket.chat/jest-presets": "workspace:~",
"@rocket.chat/livechat": "workspace:^",
"@rocket.chat/mock-providers": "workspace:^",
"@storybook/addon-a11y": "^8.4.4",
"@storybook/addon-essentials": "^8.4.4",
"@storybook/addon-interactions": "^8.4.4",
"@storybook/addon-a11y": "^8.5.3",
"@storybook/addon-essentials": "^8.5.3",
"@storybook/addon-interactions": "^8.5.3",
"@storybook/addon-styling-webpack": "^1.0.1",
"@storybook/addon-webpack5-compiler-babel": "^3.0.3",
"@storybook/react": "^8.4.4",
"@storybook/react-webpack5": "^8.4.4",
"@storybook/react": "^8.5.3",
"@storybook/react-webpack5": "^8.5.3",
"@testing-library/react": "~16.0.1",
"@testing-library/user-event": "~14.5.2",
"@types/adm-zip": "^0.5.6",
Expand Down Expand Up @@ -203,7 +203,7 @@
"react-docgen-typescript-plugin": "^1.0.8",
"sinon": "^19.0.2",
"source-map": "^0.7.4",
"storybook": "^8.4.4",
"storybook": "^8.5.3",
"stylelint": "^16.10.0",
"stylelint-config-standard": "^36.0.1",
"stylelint-order": "^6.0.4",
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/tests/data/livechat/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export const fetchInquiry = (roomId: string): Promise<ILivechatInquiryRecord> =>
};

export const createDepartment = (
agents?: { agentId: string }[],
agents?: { agentId: string; count?: number }[],
name?: string,
enabled = true,
opts: Record<string, any> = {},
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,16 @@ describe('LIVECHAT - rooms', () => {
before(async () => {
await updateSetting('Livechat_enabled', true);
await updateEESetting('Livechat_Require_Contact_Verification', 'never');
await updateSetting('Omnichannel_enable_department_removal', true);
await createAgent();
await makeAgentAvailable();
visitor = await createVisitor();

room = await createLivechatRoom(visitor.token);
});
after(async () => {
await updateSetting('Omnichannel_enable_department_removal', false);
});

describe('livechat/room', () => {
it('should fail when token is not passed as query parameter', async () => {
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/tests/end-to-end/api/livechat/01-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe('LIVECHAT - Agents', () => {
await updateSetting('Livechat_enabled', true);
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
await updateEESetting('Livechat_Require_Contact_Verification', 'never');
await updateSetting('Omnichannel_enable_department_removal', true);
agent = await createAgent();
manager = await createManager();
});
Expand All @@ -55,6 +56,7 @@ describe('LIVECHAT - Agents', () => {
});

after(async () => {
await updateSetting('Omnichannel_enable_department_removal', false);
await deleteUser(agent2.user);
});

Expand Down
5 changes: 5 additions & 0 deletions apps/meteor/tests/end-to-end/api/livechat/07-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,17 @@ describe('LIVECHAT - Queue', () => {
updateSetting('Livechat_enabled', true),
updateSetting('Livechat_Routing_Method', 'Auto_Selection'),
updateEESetting('Livechat_Require_Contact_Verification', 'never'),
updateSetting('Omnichannel_enable_department_removal', true),

// this cleanup is required since previous tests left the DB dirty
cleanupRooms(),
]),
);

after(async () => {
await updateSetting('Omnichannel_enable_department_removal', false);
});

describe('livechat/queue', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-l-room', []);
Expand Down
Loading

0 comments on commit 7cbb029

Please sign in to comment.