Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions .changeset/cool-coins-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
"@rocket.chat/models": patch
---

Fixes a bug that caused routing algorithms to ignore the `Livechat_enabled_when_agent_idle` setting, effectively ignoring idle users from being assigned to inquiries.
4 changes: 2 additions & 2 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ class LivechatClass {

async checkOnlineAgents(department?: string, agent?: { agentId: string }, skipFallbackCheck = false): Promise<boolean> {
if (agent?.agentId) {
return Users.checkOnlineAgents(agent.agentId);
return Users.checkOnlineAgents(agent.agentId, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
}

if (department) {
Expand All @@ -452,7 +452,7 @@ class LivechatClass {
return this.checkOnlineAgents(dep?.fallbackForwardDepartment);
}

return Users.checkOnlineAgents();
return Users.checkOnlineAgents(undefined, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
}

async removeRoom(rid: string) {
Expand Down
6 changes: 4 additions & 2 deletions apps/meteor/app/livechat/server/lib/getOnlineAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import type { ILivechatAgent, SelectedAgent } from '@rocket.chat/core-typings';
import { Users, LivechatDepartmentAgents } from '@rocket.chat/models';
import type { FindCursor } from 'mongodb';

import { settings } from '../../../settings/server';

export async function getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise<FindCursor<ILivechatAgent> | undefined> {
if (agent?.agentId) {
return Users.findOnlineAgents(agent.agentId);
return Users.findOnlineAgents(agent.agentId, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
}

if (department) {
Expand All @@ -20,5 +22,5 @@ export async function getOnlineAgents(department?: string, agent?: SelectedAgent

return Users.findByIds<ILivechatAgent>([...new Set(agentIds)]);
}
return Users.findOnlineAgents();
return Users.findOnlineAgents(undefined, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class AutoSelection implements IRoutingMethod {
);
}

return Users.getNextAgent(ignoreAgentId, extraQuery);
return Users.getNextAgent(ignoreAgentId, extraQuery, settings.get<boolean>('Livechat_enabled_when_agent_idle'));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Users } from '@rocket.chat/models';

import { RoutingManager } from '../../../../../../app/livechat/server/lib/RoutingManager';
import { settings } from '../../../../../../app/settings/server';
import type { IRoutingManagerConfig } from '../../../../../../definition/IRoutingManagerConfig';

/* Load Balancing Queuing method:
Expand Down Expand Up @@ -28,7 +29,11 @@ class LoadBalancing {
}

async getNextAgent(department?: string, ignoreAgentId?: string) {
const nextAgent = await Users.getNextLeastBusyAgent(department, ignoreAgentId);
const nextAgent = await Users.getNextLeastBusyAgent(
department,
ignoreAgentId,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
);
if (!nextAgent) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { IOmnichannelCustomAgent } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';

import { RoutingManager } from '../../../../../../app/livechat/server/lib/RoutingManager';
import { settings } from '../../../../../../app/settings/server';
import type { IRoutingManagerConfig } from '../../../../../../definition/IRoutingManagerConfig';

/* Load Rotation Queuing method:
Expand All @@ -27,7 +28,11 @@ class LoadRotation {
}

public async getNextAgent(department?: string, ignoreAgentId?: string): Promise<IOmnichannelCustomAgent | undefined> {
const nextAgent = await Users.getLastAvailableAgentRouted(department, ignoreAgentId);
const nextAgent = await Users.getLastAvailableAgentRouted(
department,
ignoreAgentId,
settings.get<boolean>('Livechat_enabled_when_agent_idle'),
);
if (!nextAgent) {
return;
}
Expand Down
8 changes: 8 additions & 0 deletions apps/meteor/tests/data/livechat/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,14 @@ export const createManager = (overrideUsername?: string): Promise<ILivechatAgent
});
});

export const switchLivechatStatus = async (status: 'available' | 'not-available', overrideCredentials?: Credentials): Promise<void> => {
await request
.post(api('livechat/agent.status'))
.set(overrideCredentials || credentials)
.send({ status })
.expect(200);
};

export const makeAgentAvailable = async (overrideCredentials?: Credentials): Promise<Response> => {
await restorePermissionToRoles('view-l-room');
await request
Expand Down
28 changes: 27 additions & 1 deletion apps/meteor/tests/data/users.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Credentials } from '@rocket.chat/api-client';
import type { IUser } from '@rocket.chat/core-typings';
import { UserStatus } from '@rocket.chat/core-typings';

import { api, credentials, request } from './api-data';
import { api, credentials, methodCall, request } from './api-data';
import { password } from './user';

export type TestUser<TUser extends IUser> = TUser & { username: string; emails: string[] };
Expand Down Expand Up @@ -100,3 +100,29 @@ export const setUserStatus = (overrideCredentials = credentials, status = UserSt
message: '',
status,
});

export const setUserAway = (overrideCredentials = credentials) =>
request
.post(methodCall('UserPresence:away'))
.set(overrideCredentials)
.send({
message: JSON.stringify({
method: 'UserPresence:away',
params: [],
id: 'id',
msg: 'method',
}),
});

export const setUserOnline = (overrideCredentials = credentials) =>
request
.post(methodCall('UserPresence:online'))
.set(overrideCredentials)
.send({
message: JSON.stringify({
method: 'UserPresence:online',
params: [],
id: 'id',
msg: 'method',
}),
});
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ test.describe('OC - Livechat API', () => {
await addAgentToDepartment(api, { department: departmentA, agentId: agent.data._id });
await addAgentToDepartment(api, { department: departmentB, agentId: agent2.data._id });
expect((await api.post('/settings/Livechat_offline_email', { value: '[email protected]' })).status()).toBe(200);
await api.post('/settings/Livechat_enabled_when_agent_idle', { value: false });
});

test.beforeEach(async ({ browser }, testInfo) => {
Expand Down Expand Up @@ -266,6 +267,7 @@ test.describe('OC - Livechat API', () => {
await expect((await api.post('/settings/Omnichannel_enable_department_removal', { value: true })).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);
await api.post('/settings/Livechat_enabled_when_agent_idle', { value: true });
});

// clearBusinessUnit
Expand Down Expand Up @@ -698,6 +700,7 @@ test.describe('OC - Livechat API', () => {
test.beforeAll(async ({ api }) => {
agent = await createAgent(api, 'user1');
expect((await api.post('/settings/Livechat_offline_email', { value: '[email protected]' })).status()).toBe(200);
await api.post('/settings/Livechat_enabled_when_agent_idle', { value: false });
});

test.beforeEach(async ({ browser }, testInfo) => {
Expand All @@ -723,8 +726,9 @@ test.describe('OC - Livechat API', () => {
await page.close();
});

test.afterAll(async () => {
test.afterAll(async ({ api }) => {
await agent.delete();
await api.post('/settings/Livechat_enabled_when_agent_idle', { value: true });
});

test('OC - Livechat API - onChatMaximized & onChatMinimized', async () => {
Expand Down Expand Up @@ -856,6 +860,7 @@ test.describe('OC - Livechat API', () => {
const newVisitor = createFakeVisitor();

await poAuxContext.poHomeOmnichannel.sidenav.switchStatus('offline');
await poAuxContext.poHomeOmnichannel.sidenav.switchOmnichannelStatus('offline');

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

Expand Down
91 changes: 89 additions & 2 deletions apps/meteor/tests/end-to-end/api/livechat/24-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import {
createLivechatRoom,
getLivechatRoomInfo,
makeAgentUnavailable,
switchLivechatStatus,
} from '../../../data/livechat/rooms';
import { sleep } from '../../../data/livechat/utils';
import { updateSetting } from '../../../data/permissions.helper';
import { password } from '../../../data/user';
import { createUser, deleteUser, login, setUserActiveStatus, setUserStatus } from '../../../data/users.helper';
import { createUser, deleteUser, login, setUserActiveStatus, setUserAway, setUserStatus } from '../../../data/users.helper';
import { IS_EE } from '../../../e2e/config/constants';

(IS_EE ? describe : describe.skip)('Omnichannel - Routing', () => {
Expand Down Expand Up @@ -271,7 +272,14 @@ import { IS_EE } from '../../../e2e/config/constants';
});
});

after(async () => Promise.all([deleteUser(testUser.user), deleteUser(testUser2.user), deleteUser(testUser3.user)]));
after(async () =>
Promise.all([
deleteUser(testUser.user),
deleteUser(testUser2.user),
deleteUser(testUser3.user),
updateSetting('Livechat_enabled_when_agent_idle', true),
]),
);

it('should route a room to an available agent', async () => {
const visitor = await createVisitor(testDepartment._id);
Expand Down Expand Up @@ -364,6 +372,29 @@ import { IS_EE } from '../../../e2e/config/constants';
const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.undefined;
});
it('should not route to an idle user', async () => {
await setUserStatus(testUser.credentials, UserStatus.AWAY);
await setUserAway(testUser.credentials);
await setUserStatus(testUser3.credentials, UserStatus.AWAY);
await setUserAway(testUser3.credentials);
// Agent is available but should be ignored
await switchLivechatStatus('available', testUser.credentials);

const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);

const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.undefined;
});
it('should route to an idle user', async () => {
await updateSetting('Livechat_enabled_when_agent_idle', true);

const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);

const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.an('object');
});
it('should route to another available agent if contact manager is unavailable and Omnichannel_contact_manager_routing is enabled', async () => {
await makeAgentAvailable(testUser.credentials);
const visitor = await createVisitor(testDepartment._id, faker.person.fullName(), visitorEmail);
Expand Down Expand Up @@ -505,6 +536,35 @@ import { IS_EE } from '../../../e2e/config/constants';
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser2.user._id);
});
it('should not route to an idle user', async () => {
await updateSetting('Livechat_enabled_when_agent_idle', false);
await setUserStatus(testUser.credentials, UserStatus.AWAY);
await setUserAway(testUser.credentials);
await setUserStatus(testUser2.credentials, UserStatus.AWAY);
await setUserAway(testUser2.credentials);
// Agent is available but should be ignored
await switchLivechatStatus('available', testUser.credentials);

const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);

const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.undefined;
});
it('should route to agents even if theyre idle when setting is enabled', async () => {
await updateSetting('Livechat_enabled_when_agent_idle', true);
await setUserStatus(testUser.credentials, UserStatus.AWAY);
await setUserStatus(testUser2.credentials, UserStatus.AWAY);

const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);

await sleep(5000);

const roomInfo = await getLivechatRoomInfo(room._id);
// Not checking who, just checking it's served
expect(roomInfo.servedBy).to.be.an('object');
});
});
describe('Load Rotation', () => {
before(async () => {
Expand Down Expand Up @@ -594,5 +654,32 @@ import { IS_EE } from '../../../e2e/config/constants';
expect(roomInfo.servedBy).to.be.an('object');
expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id);
});
it('should not route to an idle user', async () => {
await updateSetting('Livechat_enabled_when_agent_idle', false);
await setUserStatus(testUser.credentials, UserStatus.AWAY);
await setUserAway(testUser.credentials);
await setUserStatus(testUser2.credentials, UserStatus.AWAY);
await setUserAway(testUser2.credentials);
// Agent is available but should be ignored
await switchLivechatStatus('available', testUser.credentials);

const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);

const roomInfo = await getLivechatRoomInfo(room._id);
expect(roomInfo.servedBy).to.be.undefined;
});
it('should route to agents even if theyre idle when setting is enabled', async () => {
await updateSetting('Livechat_enabled_when_agent_idle', true);
await setUserStatus(testUser.credentials, UserStatus.AWAY);
await setUserStatus(testUser2.credentials, UserStatus.AWAY);

const visitor = await createVisitor(testDepartment._id);
const room = await createLivechatRoom(visitor.token);

const roomInfo = await getLivechatRoomInfo(room._id);
// Not checking who, just checking it's served
expect(roomInfo.servedBy).to.be.an('object');
});
});
});
13 changes: 9 additions & 4 deletions packages/model-typings/src/models/IUsersModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,12 @@ export interface IUsersModel extends IBaseModel<IUser> {
getNextLeastBusyAgent(
department: any,
ignoreAgentId: any,
isEnabledWhenAgentIdle?: boolean,
): Promise<{ agentId: string; username: string; lastRoutingTime: Date; departments: any[]; count: number }>;
getLastAvailableAgentRouted(
department: any,
ignoreAgentId: any,
isEnabledWhenAgentIdle?: boolean,
): Promise<{ agentId: string; username: string; lastRoutingTime: Date; departments: any[] }>;

setLastRoutingTime(userId: any): Promise<number>;
Expand Down Expand Up @@ -263,9 +265,8 @@ export interface IUsersModel extends IBaseModel<IUser> {
}): Promise<UpdateResult>;
findPersonalAccessTokenByTokenNameAndUserId(data: { userId: string; tokenName: string }): Promise<IPersonalAccessToken | null>;
setOperator(userId: string, operator: boolean): Promise<UpdateResult>;
checkOnlineAgents(agentId?: string): Promise<boolean>;
findOnlineAgents(agentId?: string): FindCursor<ILivechatAgent>;
countOnlineAgents(agentId: string): Promise<number>;
checkOnlineAgents(agentId?: string, isLivechatEnabledWhenIdle?: boolean): Promise<boolean>;
findOnlineAgents(agentId?: string, isLivechatEnabledWhenIdle?: boolean): FindCursor<ILivechatAgent>;
findOneBotAgent(): Promise<ILivechatAgent | null>;
findOneOnlineAgentById(
agentId: string,
Expand All @@ -274,7 +275,11 @@ export interface IUsersModel extends IBaseModel<IUser> {
): Promise<ILivechatAgent | null>;
findAgents(): FindCursor<ILivechatAgent>;
countAgents(): Promise<number>;
getNextAgent(ignoreAgentId?: string, extraQuery?: Filter<IUser>): Promise<{ agentId: string; username: string } | null>;
getNextAgent(
ignoreAgentId?: string,
extraQuery?: Filter<IUser>,
enabledWhenAgentIdle?: boolean,
): Promise<{ agentId: string; username: string } | null>;
getNextBotAgent(ignoreAgentId?: string): Promise<{ agentId: string; username: string } | null>;
setLivechatStatus(userId: string, status: ILivechatAgentStatus): Promise<UpdateResult>;
makeAgentUnavailableAndUnsetExtension(userId: string): Promise<UpdateResult>;
Expand Down
Loading
Loading