Skip to content

Commit

Permalink
[NEW] Add new endpoint 'livechat/agent.status' & deprecate changeLive…
Browse files Browse the repository at this point in the history
…chatStatus meteor method (#27047)

Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com>
  • Loading branch information
2 people authored and MartinSchoeler committed Nov 28, 2022
1 parent 4c68a32 commit a00b3f7
Show file tree
Hide file tree
Showing 17 changed files with 634 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Users } from '@rocket.chat/models';
import type { ILivechatAgent, IRole } from '@rocket.chat/core-typings';

/**
* @param {IRole['_id']} role the role id
* @param {string} text
* @param {any} pagination
*/
async function findUsers({ role, text, pagination: { offset, count, sort } }) {
async function findUsers({
role,
text,
pagination: { offset, count, sort },
}: {
role: IRole['_id'];
text?: string;
pagination: { offset: number; count: number; sort: any };
}): Promise<{ users: ILivechatAgent[]; count: number; offset: number; total: number }> {
const query = {};
if (text) {
const filterReg = new RegExp(escapeRegExp(text), 'i');
Expand All @@ -15,7 +24,7 @@ async function findUsers({ role, text, pagination: { offset, count, sort } }) {
});
}

const { cursor, totalCount } = Users.findPaginatedUsersInRolesWithQuery(role, query, {
const { cursor, totalCount } = Users.findPaginatedUsersInRolesWithQuery<ILivechatAgent>(role, query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
Expand All @@ -38,7 +47,13 @@ async function findUsers({ role, text, pagination: { offset, count, sort } }) {
total,
};
}
export async function findAgents({ text, pagination: { offset, count, sort } }) {
export async function findAgents({
text,
pagination: { offset, count, sort },
}: {
text?: string;
pagination: { offset: number; count: number; sort: any };
}): Promise<ReturnType<typeof findUsers>> {
return findUsers({
role: 'livechat-agent',
text,
Expand All @@ -50,7 +65,13 @@ export async function findAgents({ text, pagination: { offset, count, sort } })
});
}

export async function findManagers({ text, pagination: { offset, count, sort } }) {
export async function findManagers({
text,
pagination: { offset, count, sort },
}: {
text?: string;
pagination: { offset: number; count: number; sort: any };
}): Promise<ReturnType<typeof findUsers>> {
return findUsers({
role: 'livechat-manager',
text,
Expand Down
52 changes: 51 additions & 1 deletion apps/meteor/app/livechat/server/api/v1/agent.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { isGETAgentNextToken } from '@rocket.chat/rest-typings';
import { isGETAgentNextToken, isPOSTLivechatAgentStatusProps } from '@rocket.chat/rest-typings';
import { Users } from '@rocket.chat/models';
import type { ILivechatAgent } from '@rocket.chat/core-typings';
import { ILivechatAgentStatus } from '@rocket.chat/core-typings';

import { API } from '../../../../api/server';
import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat';
import { Livechat } from '../../lib/Livechat';
import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission';

API.v1.addRoute('livechat/agent.info/:rid/:token', {
async get() {
Expand Down Expand Up @@ -58,3 +62,49 @@ API.v1.addRoute(
},
},
);

API.v1.addRoute(
'livechat/agent.status',
{ authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPOSTLivechatAgentStatusProps },
{
async post() {
const { status, agentId: inputAgentId } = this.bodyParams;

const agentId = inputAgentId || this.userId;

const agent = await Users.findOneAgentById<Pick<ILivechatAgent, 'status' | 'statusLivechat'>>(agentId, {
projection: {
status: 1,
statusLivechat: 1,
},
});
if (!agent) {
return API.v1.notFound('Agent not found');
}

const newStatus: ILivechatAgentStatus =
status ||
(agent.statusLivechat === ILivechatAgentStatus.AVAILABLE ? ILivechatAgentStatus.NOT_AVAILABLE : ILivechatAgentStatus.AVAILABLE);
if (newStatus === agent.statusLivechat) {
return API.v1.success({ status: agent.statusLivechat });
}

if (agentId !== this.userId) {
if (!(await hasPermissionAsync(this.userId, 'manage-livechat-agents'))) {
return API.v1.unauthorized();
}
Livechat.setUserStatusLivechat(agentId, newStatus);

return API.v1.success({ status: newStatus });
}

if (!Livechat.allowAgentChangeServiceStatus(newStatus, agentId)) {
return API.v1.failure('error-business-hours-are-closed');
}

Livechat.setUserStatusLivechat(agentId, newStatus);

return API.v1.success({ status: newStatus });
},
},
);
15 changes: 13 additions & 2 deletions apps/meteor/app/livechat/server/methods/changeLivechatStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import { Meteor } from 'meteor/meteor';
import { Livechat } from '../lib/Livechat';
import { hasPermission } from '../../../authorization';
import Users from '../../../models/server/models/Users';
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';

Meteor.methods({
'livechat:changeLivechatStatus'({ status, agentId = Meteor.userId() } = {}) {
methodDeprecationLogger.warn(
'livechat:changeLivechatStatus is deprecated and will be removed in future versions of Rocket.Chat. Use /api/v1/livechat/agent.status REST API instead.',
);

const uid = Meteor.userId();

if (!uid || !agentId) {
Expand All @@ -22,8 +27,14 @@ Meteor.methods({
});

if (!agent) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
method: 'livechat:saveAgentInfo',
throw new Meteor.Error('error-not-allowed', 'Invalid Agent Id', {
method: 'livechat:changeLivechatStatus',
});
}

if (status && !['available', 'not-available'].includes(status)) {
throw new Meteor.Error('error-not-allowed', 'Invalid Status', {
method: 'livechat:changeLivechatStatus',
});
}

Expand Down
5 changes: 2 additions & 3 deletions apps/meteor/client/lib/userData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IUser, IUserDataEvent } from '@rocket.chat/core-typings';
import type { ILivechatAgent, IUser, IUserDataEvent } from '@rocket.chat/core-typings';
import { Serialized } from '@rocket.chat/core-typings';
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
Expand Down Expand Up @@ -29,12 +29,11 @@ type RawUserData = Serialized<
| 'active'
| 'defaultRoom'
| 'customFields'
| 'statusLivechat'
| 'oauth'
| 'createdAt'
| '_updatedAt'
| 'avatarETag'
>
> & { statusLivechat?: ILivechatAgent['statusLivechat'] }
>;

const updateUser = (userData: IUser): void => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { Sidebar } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts';
import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement } from 'react';

import { useOmnichannelAgentAvailable } from '../../../hooks/omnichannel/useOmnichannelAgentAvailable';

export const OmnichannelLivechatToggle = (): ReactElement => {
const t = useTranslation();
const agentAvailable = useOmnichannelAgentAvailable();
const changeAgentStatus = useMethod('livechat:changeLivechatStatus');
const changeAgentStatus = useEndpoint('POST', '/v1/livechat/agent.status');
const dispatchToastMessage = useToastMessageDispatch();

const handleAvailableStatusChange = useMutableCallback(async () => {
try {
await changeAgentStatus();
await changeAgentStatus({});
} catch (error: unknown) {
dispatchToastMessage({ type: 'error', message: error });
}
Expand Down
6 changes: 3 additions & 3 deletions apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ILivechatAgent, ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings';
import { Field, TextInput, Button, Margins, Box, MultiSelect, Icon, Select } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useRoute, useSetting, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
import { useToastMessageDispatch, useRoute, useSetting, useMethod, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import React, { useMemo, useRef, useState, FC, ReactElement } from 'react';

import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress';
Expand Down Expand Up @@ -76,14 +76,14 @@ const AgentEdit: FC<AgentEditProps> = ({ data, userDepartments, availableDepartm
const { handleDepartments, handleStatus, handleVoipExtension } = handlers;
const { departments, status, voipExtension } = values as {
departments: string[];
status: string;
status: ILivechatAgent['statusLivechat'];
voipExtension: string;
};

const MaxChats = useMaxChatsPerAgent();

const saveAgentInfo = useMethod('livechat:saveAgentInfo');
const saveAgentStatus = useMethod('livechat:changeLivechatStatus');
const saveAgentStatus = useEndpoint('POST', '/v1/livechat/agent.status');

const dispatchToastMessage = useToastMessageDispatch();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function findMonitors({
});
}

const { cursor, totalCount } = Users.findPaginatedUsersInRolesWithQuery('livechat-monitor', query, {
const { cursor, totalCount } = Users.findPaginatedUsersInRolesWithQuery<ILivechatMonitor>('livechat-monitor', query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/server/models/raw/Roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export class RolesRaw extends BaseRaw<IRole> implements IRolesModel {
roleId: IRole['_id'],
scope: IRoom['_id'] | undefined,
options?: any | undefined,
): Promise<FindCursor<IUser> | FindCursor<P>> {
): Promise<FindCursor<IUser | P>> {
if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) {
throw new Error('Roles.findUsersInRole method received a role scope instead of a scope value.');
}
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/server/services/meteor/service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Meteor } from 'meteor/meteor';
import { ServiceConfiguration } from 'meteor/service-configuration';
import { MongoInternals } from 'meteor/mongo';
import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import type { ILivechatAgent } from '@rocket.chat/core-typings';

import { metrics } from '../../../app/metrics';
import { ServiceClassInternal } from '../../sdk/types/ServiceClass';
Expand Down Expand Up @@ -190,7 +190,7 @@ export class MeteorService extends ServiceClassInternal implements IMeteor {
switch (clientAction) {
case 'updated':
case 'inserted':
const agent: IUser | undefined = await Users.findOneAgentById(id, {
const agent = await Users.findOneAgentById<Pick<ILivechatAgent, 'status' | 'statusLivechat'>>(id, {
projection: {
status: 1,
statusLivechat: 1,
Expand Down
74 changes: 74 additions & 0 deletions apps/meteor/tests/data/livechat/businessHours.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { api, credentials, methodCall, request } from "../api-data";
import { updateSetting } from "../permissions.helper"

export const makeDefaultBusinessHourActiveAndClosed = async () => {
// enable settings
await updateSetting('Livechat_enable_business_hours', true);
await updateSetting('Livechat_business_hour_type', 'Single');

// create business hours
const { body: { businessHour } } = await request
.get(api('livechat/business-hour?type=default'))
.set(credentials)
.send();

const workHours = businessHour.workHours as { start: string; finish: string; day: string, open: boolean }[];
const allEnabledWorkHours = workHours.map((workHour) => {
workHour.open = true;
workHour.start = '00:00';
workHour.finish = '00:01'; // if a job runs between 00:00 and 00:01, then this test will fail :P
return workHour;
});

const enabledBusinessHour = {
...businessHour,
workHours: allEnabledWorkHours,
}

await request.post(methodCall('livechat:saveBusinessHour'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:saveBusinessHour',
params: [enabledBusinessHour],
id: 'id',
msg: 'method',
}),
});
}

export const disableDefaultBusinessHour = async () => {
// disable settings
await updateSetting('Livechat_enable_business_hours', false);
await updateSetting('Livechat_business_hour_type', 'Single');

// create business hours
const { body: { businessHour } } = await request
.get(api('livechat/business-hour?type=default'))
.set(credentials)
.send();

const workHours = businessHour.workHours as { start: string; finish: string; day: string, open: boolean }[];
const allDisabledWorkHours = workHours.map((workHour) => {
workHour.open = false;
workHour.start = '00:00';
workHour.finish = '23:59';
return workHour;
});

const disabledBusinessHour = {
...businessHour,
workHours: allDisabledWorkHours,
}

await request.post(methodCall('livechat:saveBusinessHour'))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:saveBusinessHour',
params: [disabledBusinessHour],
id: 'id',
msg: 'method',
}),
});
}
13 changes: 13 additions & 0 deletions apps/meteor/tests/data/users.helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,16 @@ export const getUserStatus = (userId) =>
resolve(res.body);
});
});


export const getMe = (overrideCredential = credentials) =>
new Promise((resolve) => {
request
.get(api('me'))
.set(overrideCredential)
.expect('Content-Type', 'application/json')
.expect(200)
.end((end, res) => {
resolve(res.body);
});
});
Loading

0 comments on commit a00b3f7

Please sign in to comment.