Skip to content

Commit

Permalink
Merge pull request #32039 from RocketChat/release-6.6.5
Browse files Browse the repository at this point in the history
Release 6.6.5
  • Loading branch information
sampaiodiego authored Mar 21, 2024
2 parents c53af4f + 1b11641 commit 9c544c5
Show file tree
Hide file tree
Showing 11 changed files with 649 additions and 45 deletions.
5 changes: 5 additions & 0 deletions .changeset/bump-patch-1710964059521.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Bump @rocket.chat/meteor version.
5 changes: 5 additions & 0 deletions .changeset/empty-hounds-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/ddp-client": patch
---

fix: livechat sdk reconnect not resubscribing
5 changes: 5 additions & 0 deletions .changeset/thin-keys-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/livechat": patch
---

Fixes issue of the `setDepartment` Livechat API method not setting the store value properly (is was only setting on the guest object)
9 changes: 9 additions & 0 deletions .changeset/tough-doors-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@rocket.chat/meteor": patch
---

Introduced a new step to the queue worker: when an inquiry that's on an improper status is selected for processing, queue worker will first check its status and will attempt to fix it.
For example, if an inquiry points to a closed room, there's no point in processing, system will now remove the inquiry
If an inquiry is already taken, the inquiry will be updated to reflect the new status and clean the queue.

This prevents issues where the queue worker attempted to process an inquiry _forever_ because it was in an improper state.
77 changes: 68 additions & 9 deletions apps/meteor/server/services/omnichannel/queue.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { InquiryWithAgentInfo, IOmnichannelQueue } from '@rocket.chat/core-typings';
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { type InquiryWithAgentInfo, type IOmnichannelQueue } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { LivechatInquiry } from '@rocket.chat/models';
import { LivechatInquiry, LivechatRooms } from '@rocket.chat/models';

import { dispatchAgentDelegated } from '../../../app/livechat/server/lib/Helper';
import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager';
Expand Down Expand Up @@ -92,9 +93,11 @@ export class OmnichannelQueue implements IOmnichannelQueue {
// Note: this removes the "one-shot" behavior of queue, allowing it to take a conversation again in the future
// And sorting them by _updatedAt: -1 will make it so that the oldest inquiries are taken first
// preventing us from playing with the same inquiry over and over again
queueLogger.debug(`Inquiry ${nextInquiry._id} not taken. Unlocking and re-queueing`);
return await LivechatInquiry.unlockAndQueue(nextInquiry._id);
}

queueLogger.debug(`Inquiry ${nextInquiry._id} taken successfully. Unlocking`);
await LivechatInquiry.unlock(nextInquiry._id);
} catch (e) {
queueLogger.error({
Expand Down Expand Up @@ -123,17 +126,74 @@ export class OmnichannelQueue implements IOmnichannelQueue {
void (routingSupportsAutoAssign ? this.start() : this.stop());
}

private async reconciliation(reason: 'closed' | 'taken' | 'missing', { roomId, inquiryId }: { roomId: string; inquiryId: string }) {
switch (reason) {
case 'closed': {
queueLogger.debug({
msg: 'Room closed. Removing inquiry',
roomId,
inquiryId,
step: 'reconciliation',
});
await LivechatInquiry.removeByRoomId(roomId);
break;
}
case 'taken': {
queueLogger.debug({
msg: 'Room taken. Updating inquiry status',
roomId,
inquiryId,
step: 'reconciliation',
});
// Reconciliate served inquiries, by updating their status to taken after queue tried to pick and failed
await LivechatInquiry.takeInquiry(inquiryId);
break;
}
case 'missing': {
queueLogger.debug({
msg: 'Room from inquiry missing. Removing inquiry',
roomId,
inquiryId,
step: 'reconciliation',
});
await LivechatInquiry.removeByRoomId(roomId);
break;
}
default: {
return true;
}
}

return true;
}

private async processWaitingQueue(department: string | undefined, inquiry: InquiryWithAgentInfo) {
const queue = department || 'Public';
queueLogger.debug(`Processing items on queue ${queue}`);

queueLogger.debug(`Processing inquiry ${inquiry._id} from queue ${queue}`);
const { defaultAgent } = inquiry;
const room = await RoutingManager.delegateInquiry(inquiry, defaultAgent);

const propagateAgentDelegated = async (rid: string, agentId: string) => {
await dispatchAgentDelegated(rid, agentId);
};
const roomFromDb = await LivechatRooms.findOneById<Pick<IOmnichannelRoom, '_id' | 'servedBy' | 'closedAt'>>(inquiry.rid, {
projection: { servedBy: 1, closedAt: 1 },
});

// This is a precaution to avoid taking inquiries tied to rooms that no longer exist.
// This should never happen.
if (!roomFromDb) {
return this.reconciliation('missing', { roomId: inquiry.rid, inquiryId: inquiry._id });
}

// This is a precaution to avoid taking the same inquiry multiple times. It should not happen, but it's a safety net
if (roomFromDb.servedBy) {
return this.reconciliation('taken', { roomId: inquiry.rid, inquiryId: inquiry._id });
}

// This is another precaution. If the room is closed, we should not take it
if (roomFromDb.closedAt) {
return this.reconciliation('closed', { roomId: inquiry.rid, inquiryId: inquiry._id });
}

const room = await RoutingManager.delegateInquiry(inquiry, defaultAgent);

if (room?.servedBy) {
const {
Expand All @@ -142,13 +202,12 @@ export class OmnichannelQueue implements IOmnichannelQueue {
} = room;
queueLogger.debug(`Inquiry ${inquiry._id} taken successfully by agent ${agentId}. Notifying`);
setTimeout(() => {
void propagateAgentDelegated(rid, agentId);
void dispatchAgentDelegated(rid, agentId);
}, 1000);

return true;
}

queueLogger.debug(`Inquiry ${inquiry._id} not taken by any agent. Queueing again`);
return false;
}
}
93 changes: 69 additions & 24 deletions apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createAuxContext } from '../fixtures/createAuxContext';
import { Users } from '../fixtures/userStates';
import { HomeOmnichannel, OmnichannelLiveChatEmbedded } from '../page-objects';
import { createAgent } from '../utils/omnichannel/agents';
import { addAgentToDepartment, createDepartment } from '../utils/omnichannel/departments';
import { test, expect } from '../utils/test';

// TODO: Use official widget typing once that is merged
Expand All @@ -32,7 +33,7 @@ declare const window: Window & {
setAgent: (agent: { username: string; _id: string }) => void;
setBusinessUnit: (businessUnit?: string) => void;
setCustomField: (field: { key: string; value: string }) => void;
setDepartment: (department: { _id: string; name: string }) => void;
setDepartment: (dep: string) => void;
setGuestEmail: (email: string) => void;
setGuestName: (name: string) => void;
setGuestToken: (token: string) => void;
Expand All @@ -56,8 +57,8 @@ declare const window: Window & {
};

test.describe('OC - Livechat API', () => {
// TODO: Check if there is a way to add livechat to the global window object
// TODO: Check if there is a way to add livechat to the global window object

test.describe('Basic Widget Interactions', () => {
// Tests that rely only on the widget itself, without requiring further interaction from the main RC app
let poAuxContext: { page: Page; poHomeOmnichannel: HomeOmnichannel };
Expand Down Expand Up @@ -213,27 +214,24 @@ test.describe('OC - Livechat API', () => {
test.skip(!IS_EE, 'Enterprise Only');
// Tests that requires interaction from an agent or more
let poAuxContext: { page: Page; poHomeOmnichannel: HomeOmnichannel };
let poAuxContext2: { page: Page; poHomeOmnichannel: HomeOmnichannel };
let poLiveChat: OmnichannelLiveChatEmbedded;
let page: Page;
let depId: string;
let agent: Awaited<ReturnType<typeof createAgent>>;
let agent2: Awaited<ReturnType<typeof createAgent>>;
let departments: Awaited<ReturnType<typeof createDepartment>>[];


test.beforeAll(async ({ api }) => {
agent = await createAgent(api, 'user1')

const response = await api.post('/livechat/department', {department: {
enabled: true,
email: faker.internet.email(),
showOnRegistration: true,
showOnOfflineForm: true,
name: `new department ${Date.now()}`,
description: 'created from api',
}});

expect(response.status()).toBe(200);

const resBody = await response.json();
depId = resBody.department._id;
agent2 = await createAgent(api, 'user2')

departments = await Promise.all([createDepartment(api), createDepartment(api)]);
const [departmentA, departmentB] = departments.map(({ data }) => data);

await addAgentToDepartment(api, { department: departmentA, agentId: agent.data._id })
await addAgentToDepartment(api, { department: departmentB, agentId: agent2.data._id })

await expect((await api.post('/settings/Enable_CSP', { value: false })).status()).toBe(200);
await expect((await api.post('/settings/Livechat_offline_email', { value: '[email protected]' })).status()).toBe(200);
});
Expand All @@ -253,6 +251,11 @@ test.describe('OC - Livechat API', () => {
await poAuxContext.poHomeOmnichannel.sidenav.switchStatus('online');
}

if (testInfo.title === 'OC - Livechat API - setDepartment') {
const { page: pageCtx2 } = await createAuxContext(browser, Users.user2);
poAuxContext2 = { page: pageCtx2, poHomeOmnichannel: new HomeOmnichannel(pageCtx) };
}

await page.goto('/packages/rocketchat_livechat/assets/demo.html');
});

Expand All @@ -264,9 +267,10 @@ test.describe('OC - Livechat API', () => {
test.afterAll(async ({ api }) => {
await expect((await api.post('/settings/Enable_CSP', { value: true })).status()).toBe(200);
await agent.delete();
await agent2.delete();

await expect((await api.post('/settings/Omnichannel_enable_department_removal', { value: true })).status()).toBe(200);
const response = await api.delete(`/livechat/department/${depId}`, { name: 'TestDep', email: '[email protected]' });
expect(response.status()).toBe(200);
await Promise.all([...departments.map((department) => department.delete())]);
await expect((await api.post('/settings/Omnichannel_enable_department_removal', { value: false })).status()).toBe(200);
});

Expand Down Expand Up @@ -315,6 +319,47 @@ test.describe('OC - Livechat API', () => {
});
});

test('OC - Livechat API - setDepartment', async () => {
const [departmentA, departmentB] = departments.map(({ data }) => data);
const registerGuestVisitor = {
name: faker.person.firstName(),
email: faker.internet.email(),
token: faker.string.uuid(),
department: departmentA._id,
};

// Start Chat
await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget());
await expect(page.frameLocator('#rocketchat-iframe').getByText('Start Chat')).toBeVisible();

await poLiveChat.page.evaluate(
(registerGuestVisitor) => window.RocketChat.livechat.registerGuest(registerGuestVisitor),
registerGuestVisitor,
);

await expect(page.frameLocator('#rocketchat-iframe').getByText('Start Chat')).not.toBeVisible();

await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor');
await poLiveChat.btnSendMessageToOnlineAgent.click();

await test.step('Expect registered guest to be in dep1', async () => {
await poAuxContext.poHomeOmnichannel.sidenav.openChat(registerGuestVisitor.name);
});

const depId = departmentB._id;

await test.step('Expect setDepartment to change a guest department', async () => {
await poLiveChat.page.evaluate(
(depId) => window.RocketChat.livechat.setDepartment(depId),
depId,
);
});

await test.step('Expect registered guest to be in dep2', async () => {
await poAuxContext2.poHomeOmnichannel.sidenav.openChat(registerGuestVisitor.name);
});
});

test('OC - Livechat API - registerGuest', async ({ browser }) => {
const registerGuestVisitor = {
name: faker.person.firstName(),
Expand Down Expand Up @@ -495,14 +540,14 @@ test.describe('OC - Livechat API', () => {

const { page: pageCtx } = await createAuxContext(browser, Users.user1);
poAuxContext = { page: pageCtx, poHomeOmnichannel: new HomeOmnichannel(pageCtx) };

// This is needed since the livechat will not react to online/offline status changes if already loaded in a page
if (testInfo.title === 'Expect onOfflineFormSubmit to trigger callback') {
await poAuxContext.poHomeOmnichannel.sidenav.switchStatus('offline');
} else {
await poAuxContext.poHomeOmnichannel.sidenav.switchStatus('online');
}

await page.goto('/packages/rocketchat_livechat/assets/demo.html');
});

Expand Down Expand Up @@ -635,7 +680,7 @@ test.describe('OC - Livechat API', () => {
await poLiveChat.sendMessage(newVisitor, false);
await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor');
await poLiveChat.btnSendMessageToOnlineAgent.click();


const watchForTrigger = page.waitForFunction(() => window.onAgentStatusChange === true);

Expand Down Expand Up @@ -717,5 +762,5 @@ test.describe('OC - Livechat API', () => {
});
});
});

});
Loading

0 comments on commit 9c544c5

Please sign in to comment.