snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
192
apps/meteor/ee/server/api/v1/canned-responses.ts
Normal file
192
apps/meteor/ee/server/api/v1/canned-responses.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import type { ILivechatDepartment, IOmnichannelCannedResponse, IUser } from '@rocket.chat/core-typings';
|
||||
import { isPOSTCannedResponsesProps, isCannedResponsesProps, isDELETECannedResponsesProps } from '@rocket.chat/rest-typings';
|
||||
import type { PaginatedResult, PaginatedRequest } from '@rocket.chat/rest-typings';
|
||||
|
||||
import { API } from '../../../../server/api';
|
||||
import { getPaginationItems } from '../../../../server/api/lib/getPaginationItems';
|
||||
import { removeCannedResponse } from '../../meteor-methods/removeCannedResponse';
|
||||
import { saveCannedResponse } from '../../meteor-methods/saveCannedResponse';
|
||||
import { findAllCannedResponses, findAllCannedResponsesFilter, findOneCannedResponse } from '../lib/canned-responses';
|
||||
|
||||
declare module '@rocket.chat/rest-typings' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface Endpoints {
|
||||
'/v1/canned-responses': {
|
||||
GET: (
|
||||
params: PaginatedRequest<{
|
||||
shortcut?: string;
|
||||
text?: string;
|
||||
scope?: string;
|
||||
createdBy?: IUser['username'];
|
||||
tags?: any;
|
||||
departmentId?: ILivechatDepartment['_id'];
|
||||
}>,
|
||||
) => PaginatedResult<{
|
||||
cannedResponses: IOmnichannelCannedResponse[];
|
||||
}>;
|
||||
POST: (params: {
|
||||
_id?: IOmnichannelCannedResponse['_id'];
|
||||
shortcut: string;
|
||||
text: string;
|
||||
scope: string;
|
||||
tags?: any;
|
||||
departmentId?: ILivechatDepartment['_id'];
|
||||
}) => void;
|
||||
DELETE: (params: { _id: IOmnichannelCannedResponse['_id'] }) => void;
|
||||
};
|
||||
'/v1/canned-responses/:_id': {
|
||||
GET: () => {
|
||||
cannedResponse: IOmnichannelCannedResponse;
|
||||
};
|
||||
DELETE: () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
API.v1.addRoute(
|
||||
'canned-responses.get',
|
||||
{ authRequired: true, permissionsRequired: ['view-canned-responses'], license: ['canned-responses'] },
|
||||
{
|
||||
async get() {
|
||||
return API.v1.success({
|
||||
responses: await findAllCannedResponses({ userId: this.userId }),
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @openapi
|
||||
* /api/v1/canned-responses:
|
||||
* delete:
|
||||
* deprecated: true
|
||||
* security:
|
||||
* $ref: '#/security/authenticated'
|
||||
* parameters:
|
||||
* - in: body
|
||||
* name: body
|
||||
* description: |
|
||||
* **_id** (required): Canned Response ID to be removed.
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - _id
|
||||
* properties:
|
||||
* _id:
|
||||
* type: string
|
||||
* tags:
|
||||
* - Canned_Responses
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Successful Response
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* example: success
|
||||
* data:
|
||||
* type: object
|
||||
* description: The response data
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* 401:
|
||||
* $ref: '#/responses/Unauthorized'
|
||||
* 403:
|
||||
* $ref: '#/responses/Forbidden'
|
||||
* 404:
|
||||
* $ref: '#/responses/NotFound'
|
||||
* 500:
|
||||
* $ref: '#/responses/InternalServerError'
|
||||
*/
|
||||
API.v1.addRoute(
|
||||
'canned-responses',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: { GET: ['view-canned-responses'], POST: ['save-canned-responses'], DELETE: ['remove-canned-responses'] },
|
||||
validateParams: { POST: isPOSTCannedResponsesProps, DELETE: isDELETECannedResponsesProps, GET: isCannedResponsesProps },
|
||||
license: ['canned-responses'],
|
||||
deprecations: { DELETE: { version: '8.0.0', alternatives: ['/v1/canned-responses/:_id'] } },
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { sort, fields } = await this.parseJsonQuery();
|
||||
const { shortcut, text, scope, tags, departmentId, createdBy } = this.queryParams;
|
||||
const { cannedResponses, total } = await findAllCannedResponsesFilter({
|
||||
shortcut,
|
||||
text,
|
||||
scope,
|
||||
tags,
|
||||
departmentId,
|
||||
userId: this.userId,
|
||||
createdBy,
|
||||
options: {
|
||||
sort,
|
||||
offset,
|
||||
count,
|
||||
fields,
|
||||
},
|
||||
});
|
||||
return API.v1.success({
|
||||
cannedResponses,
|
||||
count: cannedResponses.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
async post() {
|
||||
const { _id, shortcut, text, scope, departmentId, tags } = this.bodyParams;
|
||||
await saveCannedResponse(
|
||||
this.userId,
|
||||
{
|
||||
shortcut,
|
||||
text,
|
||||
scope,
|
||||
...(tags && { tags }),
|
||||
...(departmentId && { departmentId }),
|
||||
},
|
||||
_id,
|
||||
);
|
||||
return API.v1.success();
|
||||
},
|
||||
// deprecated
|
||||
async delete() {
|
||||
const { _id } = this.bodyParams;
|
||||
await removeCannedResponse(this.userId, _id);
|
||||
return API.v1.success();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'canned-responses/:_id',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: { GET: ['view-canned-responses'], DELETE: ['remove-canned-responses'] },
|
||||
license: ['canned-responses'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { _id } = this.urlParams;
|
||||
const cannedResponse = await findOneCannedResponse({
|
||||
userId: this.userId,
|
||||
_id,
|
||||
});
|
||||
|
||||
if (!cannedResponse) {
|
||||
return API.v1.notFound();
|
||||
}
|
||||
|
||||
return API.v1.success({ cannedResponse });
|
||||
},
|
||||
async delete() {
|
||||
const { _id } = this.urlParams;
|
||||
await removeCannedResponse(this.userId, _id);
|
||||
return API.v1.success();
|
||||
},
|
||||
},
|
||||
);
|
||||
32
apps/meteor/ee/server/api/v1/middlewares/license.ts
Normal file
32
apps/meteor/ee/server/api/v1/middlewares/license.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { LicenseManager } from '@rocket.chat/license';
|
||||
import type { MiddlewareHandler } from 'hono';
|
||||
|
||||
import type { FailureResult, TypedOptions } from '../../../../../server/api/definition';
|
||||
|
||||
export const license =
|
||||
(options: TypedOptions, licenseManager: LicenseManager): MiddlewareHandler =>
|
||||
async (c, next) => {
|
||||
if (!options.license) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const license = options.license.every((license) => licenseManager.hasModule(license));
|
||||
|
||||
const failure: FailureResult<{
|
||||
error: string;
|
||||
errorType: string;
|
||||
}> = {
|
||||
statusCode: 400,
|
||||
body: {
|
||||
success: false,
|
||||
error: 'This is an enterprise feature [error-action-not-allowed]',
|
||||
errorType: 'error-action-not-allowed',
|
||||
},
|
||||
};
|
||||
|
||||
if (!license) {
|
||||
return c.json(failure.body, failure.statusCode);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
127
apps/meteor/ee/server/api/v1/omnichannel/agents.ts
Normal file
127
apps/meteor/ee/server/api/v1/omnichannel/agents.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
isLivechatAnalyticsAgentsTotalServiceTimeProps,
|
||||
isLivechatAnalyticsAgentsAverageServiceTimeProps,
|
||||
isLivechatAnalyticsAgentsAvailableForServiceHistoryProps,
|
||||
} from '@rocket.chat/rest-typings';
|
||||
|
||||
import { API } from '../../../../../server/api';
|
||||
import { getPaginationItems } from '../../../../../server/api/lib/getPaginationItems';
|
||||
import {
|
||||
findAllAverageServiceTimeAsync,
|
||||
findAllServiceTimeAsync,
|
||||
findAvailableServiceTimeHistoryAsync,
|
||||
} from '../../../../../server/lib/omnichannel/analytics/agents';
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/agents/average-service-time',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-manager'],
|
||||
validateParams: isLivechatAnalyticsAgentsAverageServiceTimeProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { start, end } = this.queryParams;
|
||||
|
||||
if (isNaN(Date.parse(start))) {
|
||||
return API.v1.failure('The "start" query parameter must be a valid date.');
|
||||
}
|
||||
const startDate = new Date(start);
|
||||
|
||||
if (isNaN(Date.parse(end))) {
|
||||
return API.v1.failure('The "end" query parameter must be a valid date.');
|
||||
}
|
||||
const endDate = new Date(end);
|
||||
|
||||
const { agents, total } = await findAllAverageServiceTimeAsync({
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
options: { offset, count },
|
||||
});
|
||||
return API.v1.success({
|
||||
agents,
|
||||
count: agents.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/agents/total-service-time',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-manager'],
|
||||
validateParams: isLivechatAnalyticsAgentsTotalServiceTimeProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { start, end } = this.queryParams;
|
||||
|
||||
if (isNaN(Date.parse(start))) {
|
||||
return API.v1.failure('The "start" query parameter must be a valid date.');
|
||||
}
|
||||
const startDate = new Date(start);
|
||||
|
||||
if (isNaN(Date.parse(end))) {
|
||||
return API.v1.failure('The "end" query parameter must be a valid date.');
|
||||
}
|
||||
const endDate = new Date(end);
|
||||
|
||||
const { agents, total } = await findAllServiceTimeAsync({
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
options: { offset, count },
|
||||
});
|
||||
return API.v1.success({
|
||||
agents,
|
||||
count: agents.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/agents/available-for-service-history',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-manager'],
|
||||
validateParams: isLivechatAnalyticsAgentsAvailableForServiceHistoryProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { start, end } = this.queryParams;
|
||||
const { fullReport } = this.queryParams;
|
||||
|
||||
if (isNaN(Date.parse(start))) {
|
||||
return API.v1.failure('The "start" query parameter must be a valid date.');
|
||||
}
|
||||
|
||||
if (isNaN(Date.parse(end))) {
|
||||
return API.v1.failure('The "end" query parameter must be a valid date.');
|
||||
}
|
||||
|
||||
const { agents, total } = await findAvailableServiceTimeHistoryAsync({
|
||||
start,
|
||||
end,
|
||||
fullReport: fullReport === 'true',
|
||||
options: { offset, count },
|
||||
});
|
||||
return API.v1.success({
|
||||
agents,
|
||||
count: agents.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
44
apps/meteor/ee/server/api/v1/omnichannel/business-hours.ts
Normal file
44
apps/meteor/ee/server/api/v1/omnichannel/business-hours.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
|
||||
import type { PaginatedRequest } from '@rocket.chat/rest-typings';
|
||||
|
||||
import { API } from '../../../../../server/api';
|
||||
import { getPaginationItems } from '../../../../../server/api/lib/getPaginationItems';
|
||||
import { findBusinessHours } from '../../../lib/omnichannel/business-hour/lib/business-hour';
|
||||
|
||||
declare module '@rocket.chat/rest-typings' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface Endpoints {
|
||||
'/v1/livechat/business-hours': {
|
||||
GET: (params: PaginatedRequest) => {
|
||||
businessHours: ILivechatBusinessHour[];
|
||||
count: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/business-hours',
|
||||
{ authRequired: true, permissionsRequired: ['view-livechat-business-hours'], license: ['livechat-enterprise'] },
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
const { name } = this.queryParams;
|
||||
|
||||
return API.v1.success(
|
||||
await findBusinessHours(
|
||||
this.userId,
|
||||
{
|
||||
offset,
|
||||
count,
|
||||
sort,
|
||||
},
|
||||
name,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
93
apps/meteor/ee/server/api/v1/omnichannel/contacts.ts
Normal file
93
apps/meteor/ee/server/api/v1/omnichannel/contacts.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { ILivechatContactVisitorAssociation } from '@rocket.chat/core-typings';
|
||||
import { ContactVisitorAssociationSchema, ajv } from '@rocket.chat/rest-typings';
|
||||
|
||||
import { changeContactBlockStatus, closeBlockedRoom, ensureSingleContactLicense } from './lib/contacts';
|
||||
import { API } from '../../../../../server/api';
|
||||
import { logger } from '../../../lib/omnichannel/logger';
|
||||
|
||||
type blockContactProps = {
|
||||
visitor: ILivechatContactVisitorAssociation;
|
||||
};
|
||||
|
||||
const blockContactSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
visitor: ContactVisitorAssociationSchema,
|
||||
},
|
||||
required: ['visitor'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const isBlockContactProps = ajv.compile<blockContactProps>(blockContactSchema);
|
||||
|
||||
declare module '@rocket.chat/rest-typings' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface Endpoints {
|
||||
'/v1/omnichannel/contacts.block': {
|
||||
POST: (params: blockContactProps) => void;
|
||||
};
|
||||
'/v1/omnichannel/contacts.unblock': {
|
||||
POST: (params: blockContactProps) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
API.v1.addRoute(
|
||||
'omnichannel/contacts.block',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['block-livechat-contact'],
|
||||
validateParams: isBlockContactProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async post() {
|
||||
ensureSingleContactLicense();
|
||||
const { visitor } = this.bodyParams;
|
||||
const { user } = this;
|
||||
|
||||
await changeContactBlockStatus({
|
||||
visitor,
|
||||
block: true,
|
||||
});
|
||||
logger.info({
|
||||
msg: 'Visitor blocked',
|
||||
visitorId: visitor.visitorId,
|
||||
userId: user._id,
|
||||
});
|
||||
|
||||
await closeBlockedRoom(visitor, user);
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'omnichannel/contacts.unblock',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['unblock-livechat-contact'],
|
||||
validateParams: isBlockContactProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async post() {
|
||||
ensureSingleContactLicense();
|
||||
const { visitor } = this.bodyParams;
|
||||
const { user } = this;
|
||||
|
||||
await changeContactBlockStatus({
|
||||
visitor,
|
||||
block: false,
|
||||
});
|
||||
logger.info({
|
||||
msg: 'Visitor unblocked',
|
||||
visitorId: visitor.visitorId,
|
||||
userId: user._id,
|
||||
});
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
},
|
||||
);
|
||||
344
apps/meteor/ee/server/api/v1/omnichannel/departments.ts
Normal file
344
apps/meteor/ee/server/api/v1/omnichannel/departments.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import {
|
||||
isLivechatAnalyticsDepartmentsAmountOfChatsProps,
|
||||
isLivechatAnalyticsDepartmentsAverageServiceTimeProps,
|
||||
isLivechatAnalyticsDepartmentsAverageChatDurationTimeProps,
|
||||
isLivechatAnalyticsDepartmentsTotalServiceTimeProps,
|
||||
isLivechatAnalyticsDepartmentsAverageWaitingTimeProps,
|
||||
isLivechatAnalyticsDepartmentsTotalTransferredChatsProps,
|
||||
isLivechatAnalyticsDepartmentsTotalAbandonedChatsProps,
|
||||
isLivechatAnalyticsDepartmentsPercentageAbandonedChatsProps,
|
||||
} from '@rocket.chat/rest-typings';
|
||||
|
||||
import { API } from '../../../../../server/api';
|
||||
import { getPaginationItems } from '../../../../../server/api/lib/getPaginationItems';
|
||||
import {
|
||||
findAllRoomsAsync,
|
||||
findAllAverageServiceTimeAsync,
|
||||
findAllServiceTimeAsync,
|
||||
findAllAverageWaitingTimeAsync,
|
||||
findAllNumberOfTransferredRoomsAsync,
|
||||
findAllNumberOfAbandonedRoomsAsync,
|
||||
findPercentageOfAbandonedRoomsAsync,
|
||||
findAllAverageOfChatDurationTimeAsync,
|
||||
} from '../../../../../server/lib/omnichannel/analytics/departments';
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/departments/amount-of-chats',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-manager'],
|
||||
validateParams: isLivechatAnalyticsDepartmentsAmountOfChatsProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { start, end } = this.queryParams;
|
||||
const { answered, departmentId } = this.queryParams;
|
||||
|
||||
if (isNaN(Date.parse(start))) {
|
||||
return API.v1.failure('The "start" query parameter must be a valid date.');
|
||||
}
|
||||
const startDate = new Date(start);
|
||||
|
||||
if (isNaN(Date.parse(end))) {
|
||||
return API.v1.failure('The "end" query parameter must be a valid date.');
|
||||
}
|
||||
const endDate = new Date(end);
|
||||
|
||||
const { departments, total } = await findAllRoomsAsync({
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
answered: answered === 'true',
|
||||
departmentId,
|
||||
options: { offset, count },
|
||||
});
|
||||
return API.v1.success({
|
||||
departments,
|
||||
count: departments.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/departments/average-service-time',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-manager'],
|
||||
validateParams: isLivechatAnalyticsDepartmentsAverageServiceTimeProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { start, end } = this.queryParams;
|
||||
const { departmentId } = this.queryParams;
|
||||
|
||||
if (isNaN(Date.parse(start))) {
|
||||
return API.v1.failure('The "start" query parameter must be a valid date.');
|
||||
}
|
||||
const startDate = new Date(start);
|
||||
|
||||
if (isNaN(Date.parse(end))) {
|
||||
return API.v1.failure('The "end" query parameter must be a valid date.');
|
||||
}
|
||||
const endDate = new Date(end);
|
||||
|
||||
const { departments, total } = await findAllAverageServiceTimeAsync({
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
departmentId,
|
||||
options: { offset, count },
|
||||
});
|
||||
return API.v1.success({
|
||||
departments,
|
||||
count: departments.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/departments/average-chat-duration-time',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-manager'],
|
||||
validateParams: isLivechatAnalyticsDepartmentsAverageChatDurationTimeProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { start, end } = this.queryParams;
|
||||
const { departmentId } = this.queryParams;
|
||||
|
||||
if (isNaN(Date.parse(start))) {
|
||||
return API.v1.failure('The "start" query parameter must be a valid date.');
|
||||
}
|
||||
const startDate = new Date(start);
|
||||
|
||||
if (isNaN(Date.parse(end))) {
|
||||
return API.v1.failure('The "end" query parameter must be a valid date.');
|
||||
}
|
||||
const endDate = new Date(end);
|
||||
|
||||
const { departments, total } = await findAllAverageOfChatDurationTimeAsync({
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
departmentId,
|
||||
options: { offset, count },
|
||||
});
|
||||
return API.v1.success({
|
||||
departments,
|
||||
count: departments.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/departments/total-service-time',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-manager'],
|
||||
validateParams: isLivechatAnalyticsDepartmentsTotalServiceTimeProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { start, end } = this.queryParams;
|
||||
const { departmentId } = this.queryParams;
|
||||
|
||||
if (isNaN(Date.parse(start))) {
|
||||
return API.v1.failure('The "start" query parameter must be a valid date.');
|
||||
}
|
||||
const startDate = new Date(start);
|
||||
|
||||
if (isNaN(Date.parse(end))) {
|
||||
return API.v1.failure('The "end" query parameter must be a valid date.');
|
||||
}
|
||||
const endDate = new Date(end);
|
||||
|
||||
const { departments, total } = await findAllServiceTimeAsync({
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
departmentId,
|
||||
options: { offset, count },
|
||||
});
|
||||
return API.v1.success({
|
||||
departments,
|
||||
count: departments.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/departments/average-waiting-time',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-manager'],
|
||||
validateParams: isLivechatAnalyticsDepartmentsAverageWaitingTimeProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { start, end } = this.queryParams;
|
||||
const { departmentId } = this.queryParams;
|
||||
|
||||
if (isNaN(Date.parse(start))) {
|
||||
return API.v1.failure('The "start" query parameter must be a valid date.');
|
||||
}
|
||||
const startDate = new Date(start);
|
||||
|
||||
if (isNaN(Date.parse(end))) {
|
||||
return API.v1.failure('The "end" query parameter must be a valid date.');
|
||||
}
|
||||
const endDate = new Date(end);
|
||||
|
||||
const { departments, total } = await findAllAverageWaitingTimeAsync({
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
departmentId,
|
||||
options: { offset, count },
|
||||
});
|
||||
return API.v1.success({
|
||||
departments,
|
||||
count: departments.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/departments/total-transferred-chats',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-manager'],
|
||||
validateParams: isLivechatAnalyticsDepartmentsTotalTransferredChatsProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { start, end } = this.queryParams;
|
||||
const { departmentId } = this.queryParams;
|
||||
|
||||
if (isNaN(Date.parse(start))) {
|
||||
return API.v1.failure('The "start" query parameter must be a valid date.');
|
||||
}
|
||||
const startDate = new Date(start);
|
||||
|
||||
if (isNaN(Date.parse(end))) {
|
||||
return API.v1.failure('The "end" query parameter must be a valid date.');
|
||||
}
|
||||
const endDate = new Date(end);
|
||||
|
||||
const { departments, total } = await findAllNumberOfTransferredRoomsAsync({
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
departmentId,
|
||||
options: { offset, count },
|
||||
});
|
||||
return API.v1.success({
|
||||
departments,
|
||||
count: departments.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/departments/total-abandoned-chats',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-manager'],
|
||||
validateParams: isLivechatAnalyticsDepartmentsTotalAbandonedChatsProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { start, end } = this.queryParams;
|
||||
const { departmentId } = this.queryParams;
|
||||
|
||||
if (isNaN(Date.parse(start))) {
|
||||
return API.v1.failure('The "start" query parameter must be a valid date.');
|
||||
}
|
||||
const startDate = new Date(start);
|
||||
|
||||
if (isNaN(Date.parse(end))) {
|
||||
return API.v1.failure('The "end" query parameter must be a valid date.');
|
||||
}
|
||||
const endDate = new Date(end);
|
||||
|
||||
const { departments, total } = await findAllNumberOfAbandonedRoomsAsync({
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
departmentId,
|
||||
options: { offset, count },
|
||||
});
|
||||
return API.v1.success({
|
||||
departments,
|
||||
count: departments.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/departments/percentage-abandoned-chats',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-manager'],
|
||||
validateParams: isLivechatAnalyticsDepartmentsPercentageAbandonedChatsProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { start, end } = this.queryParams;
|
||||
const { departmentId } = this.queryParams;
|
||||
|
||||
if (isNaN(Date.parse(start))) {
|
||||
return API.v1.failure('The "start" query parameter must be a valid date.');
|
||||
}
|
||||
const startDate = new Date(start);
|
||||
|
||||
if (isNaN(Date.parse(end))) {
|
||||
return API.v1.failure('The "end" query parameter must be a valid date.');
|
||||
}
|
||||
const endDate = new Date(end);
|
||||
|
||||
const { departments, total } = await findPercentageOfAbandonedRoomsAsync({
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
departmentId,
|
||||
options: { offset, count },
|
||||
});
|
||||
return API.v1.success({
|
||||
departments,
|
||||
count: departments.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
15
apps/meteor/ee/server/api/v1/omnichannel/index.ts
Normal file
15
apps/meteor/ee/server/api/v1/omnichannel/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import './agents';
|
||||
import './departments';
|
||||
import './inquiries';
|
||||
import './monitors';
|
||||
import './sla';
|
||||
import './priorities';
|
||||
import './tags';
|
||||
import './units';
|
||||
import './business-hours';
|
||||
import './rooms';
|
||||
import './transcript';
|
||||
import './reports';
|
||||
import './triggers';
|
||||
import './contacts';
|
||||
import './outbound';
|
||||
27
apps/meteor/ee/server/api/v1/omnichannel/inquiries.ts
Normal file
27
apps/meteor/ee/server/api/v1/omnichannel/inquiries.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { setSLAToInquiry } from './lib/inquiries';
|
||||
import { API } from '../../../../../server/api';
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/inquiry.setSLA',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: {
|
||||
PUT: { permissions: ['view-l-room', 'manage-livechat-sla'], operation: 'hasAny' },
|
||||
},
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async put() {
|
||||
const { roomId, sla } = this.bodyParams;
|
||||
if (!roomId) {
|
||||
return API.v1.failure("The 'roomId' param is required");
|
||||
}
|
||||
await setSLAToInquiry({
|
||||
userId: this.userId,
|
||||
roomId,
|
||||
sla,
|
||||
});
|
||||
return API.v1.success();
|
||||
},
|
||||
},
|
||||
);
|
||||
36
apps/meteor/ee/server/api/v1/omnichannel/lib/contacts.ts
Normal file
36
apps/meteor/ee/server/api/v1/omnichannel/lib/contacts.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { IUser, ILivechatContactVisitorAssociation } from '@rocket.chat/core-typings';
|
||||
import { License } from '@rocket.chat/license';
|
||||
import { LivechatContacts, LivechatRooms, LivechatVisitors } from '@rocket.chat/models';
|
||||
|
||||
import { i18n } from '../../../../../../server/lib/i18n';
|
||||
import { closeRoom } from '../../../../../../server/lib/omnichannel/closeRoom';
|
||||
|
||||
export async function changeContactBlockStatus({ block, visitor }: { visitor: ILivechatContactVisitorAssociation; block: boolean }) {
|
||||
const result = await LivechatContacts.setChannelBlockStatus(visitor, block);
|
||||
|
||||
if (!result.modifiedCount) {
|
||||
throw new Error('error-contact-not-found');
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureSingleContactLicense() {
|
||||
if (!License.hasModule('contact-id-verification')) {
|
||||
throw new Error('error-action-not-allowed');
|
||||
}
|
||||
}
|
||||
|
||||
export async function closeBlockedRoom(association: ILivechatContactVisitorAssociation, user: IUser) {
|
||||
const visitor = await LivechatVisitors.findOneById(association.visitorId);
|
||||
|
||||
if (!visitor) {
|
||||
throw new Error('error-visitor-not-found');
|
||||
}
|
||||
|
||||
const room = await LivechatRooms.findOneOpenByContactChannelVisitor(association);
|
||||
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
||||
return closeRoom({ room, visitor, comment: i18n.t('close-blocked-room-comment'), user });
|
||||
}
|
||||
62
apps/meteor/ee/server/api/v1/omnichannel/lib/dashboards.ts
Normal file
62
apps/meteor/ee/server/api/v1/omnichannel/lib/dashboards.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { ReportResult, ReportWithUnmatchingElements, IOmnichannelRoom } from '@rocket.chat/core-typings';
|
||||
import { LivechatRooms } from '@rocket.chat/models';
|
||||
import mem from 'mem';
|
||||
import type { Filter } from 'mongodb';
|
||||
|
||||
type AggParams = { start: Date; end: Date; sort: Record<string, 1 | -1>; extraQuery: Filter<IOmnichannelRoom> };
|
||||
|
||||
const defaultValue = { data: [], total: 0 };
|
||||
export const findAllConversationsBySource = async ({ start, end, extraQuery }: Omit<AggParams, 'sort'>): Promise<ReportResult> => {
|
||||
return (await LivechatRooms.getConversationsBySource(start, end, extraQuery).toArray())[0] || defaultValue;
|
||||
};
|
||||
|
||||
export const findAllConversationsByStatus = async ({ start, end, extraQuery }: Omit<AggParams, 'sort'>): Promise<ReportResult> => {
|
||||
return (await LivechatRooms.getConversationsByStatus(start, end, extraQuery).toArray())[0] || defaultValue;
|
||||
};
|
||||
|
||||
export const findAllConversationsByDepartment = async ({
|
||||
start,
|
||||
end,
|
||||
sort,
|
||||
extraQuery,
|
||||
}: AggParams): Promise<ReportWithUnmatchingElements> => {
|
||||
const [result, total] = await Promise.all([
|
||||
LivechatRooms.getConversationsByDepartment(start, end, sort, extraQuery).toArray(),
|
||||
LivechatRooms.getTotalConversationsWithoutDepartmentBetweenDates(start, end, extraQuery),
|
||||
]);
|
||||
|
||||
return {
|
||||
...(result?.[0] || defaultValue),
|
||||
unspecified: total || 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const findAllConversationsByTags = async ({ start, end, sort, extraQuery }: AggParams): Promise<ReportWithUnmatchingElements> => {
|
||||
const [result, total] = await Promise.all([
|
||||
LivechatRooms.getConversationsByTags(start, end, sort, extraQuery).toArray(),
|
||||
LivechatRooms.getConversationsWithoutTagsBetweenDate(start, end, extraQuery),
|
||||
]);
|
||||
|
||||
return {
|
||||
...(result?.[0] || defaultValue),
|
||||
unspecified: total || 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const findAllConversationsByAgents = async ({ start, end, sort, extraQuery }: AggParams): Promise<ReportWithUnmatchingElements> => {
|
||||
const [result, total] = await Promise.all([
|
||||
LivechatRooms.getConversationsByAgents(start, end, sort, extraQuery).toArray(),
|
||||
LivechatRooms.getTotalConversationsWithoutAgentsBetweenDate(start, end, extraQuery),
|
||||
]);
|
||||
|
||||
return {
|
||||
...(result?.[0] || defaultValue),
|
||||
unspecified: total || 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const findAllConversationsBySourceCached = mem(findAllConversationsBySource, { maxAge: 60000, cacheKey: JSON.stringify });
|
||||
export const findAllConversationsByStatusCached = mem(findAllConversationsByStatus, { maxAge: 60000, cacheKey: JSON.stringify });
|
||||
export const findAllConversationsByDepartmentCached = mem(findAllConversationsByDepartment, { maxAge: 60000, cacheKey: JSON.stringify });
|
||||
export const findAllConversationsByTagsCached = mem(findAllConversationsByTags, { maxAge: 60000, cacheKey: JSON.stringify });
|
||||
export const findAllConversationsByAgentsCached = mem(findAllConversationsByAgents, { maxAge: 60000, cacheKey: JSON.stringify });
|
||||
11
apps/meteor/ee/server/api/v1/omnichannel/lib/definition.ts
Normal file
11
apps/meteor/ee/server/api/v1/omnichannel/lib/definition.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface IPagination {
|
||||
offset: number;
|
||||
count: number;
|
||||
sort: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface IPaginatedResponse {
|
||||
count: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
}
|
||||
45
apps/meteor/ee/server/api/v1/omnichannel/lib/departments.ts
Normal file
45
apps/meteor/ee/server/api/v1/omnichannel/lib/departments.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { LivechatDepartment, LivechatDepartmentAgents, LivechatUnit } from '@rocket.chat/models';
|
||||
|
||||
import { helperLogger } from '../../../../lib/omnichannel/logger';
|
||||
|
||||
export const getDepartmentsWhichUserCanAccess = async (userId: string, includeDisabled = false): Promise<string[]> => {
|
||||
const departments = await LivechatDepartmentAgents.find(
|
||||
{
|
||||
agentId: userId,
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
departmentId: 1,
|
||||
},
|
||||
},
|
||||
).toArray();
|
||||
|
||||
const monitoredDepartments = await LivechatUnit.findMonitoredDepartmentsByMonitorId(userId, includeDisabled);
|
||||
const combinedDepartments = [
|
||||
...departments.map((department) => department.departmentId),
|
||||
...monitoredDepartments.map((department) => department._id),
|
||||
];
|
||||
|
||||
return [...new Set(combinedDepartments)];
|
||||
};
|
||||
|
||||
export const hasAccessToDepartment = async (userId: string, departmentId: string): Promise<boolean> => {
|
||||
const department = await LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(userId, departmentId, { projection: { _id: 1 } });
|
||||
if (department) {
|
||||
helperLogger.debug({
|
||||
msg: 'User has access to department as agent',
|
||||
userId,
|
||||
departmentId,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const monitorAccess = await LivechatDepartment.checkIfMonitorIsMonitoringDepartmentById(userId, departmentId);
|
||||
helperLogger.debug({
|
||||
msg: 'User monitor access status for department',
|
||||
userId,
|
||||
departmentId,
|
||||
hasAccess: monitorAccess,
|
||||
});
|
||||
return monitorAccess;
|
||||
};
|
||||
30
apps/meteor/ee/server/api/v1/omnichannel/lib/inquiries.ts
Normal file
30
apps/meteor/ee/server/api/v1/omnichannel/lib/inquiries.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { LivechatInquiry, Users, OmnichannelServiceLevelAgreements } from '@rocket.chat/models';
|
||||
|
||||
import { updateRoomSLA } from './sla';
|
||||
|
||||
export async function setSLAToInquiry({ userId, roomId, sla }: { userId: string; roomId: string; sla?: string }): Promise<void> {
|
||||
const inquiry = await LivechatInquiry.findOneByRoomId(roomId, { projection: { status: 1 } });
|
||||
if (!inquiry || inquiry.status !== 'queued') {
|
||||
throw new Error('error-invalid-inquiry');
|
||||
}
|
||||
|
||||
const slaData = sla && (await OmnichannelServiceLevelAgreements.findOneByIdOrName(sla));
|
||||
if (!slaData) {
|
||||
throw new Error('error-invalid-sla');
|
||||
}
|
||||
|
||||
const user = await Users.findOneById(userId, { projection: { _id: 1, username: 1, name: 1 } });
|
||||
if (!user?.username) {
|
||||
throw new Error('error-invalid-user');
|
||||
}
|
||||
|
||||
await updateRoomSLA(
|
||||
roomId,
|
||||
{
|
||||
_id: user._id,
|
||||
name: user.name || '',
|
||||
username: user.username,
|
||||
},
|
||||
slaData,
|
||||
);
|
||||
}
|
||||
71
apps/meteor/ee/server/api/v1/omnichannel/lib/monitors.ts
Normal file
71
apps/meteor/ee/server/api/v1/omnichannel/lib/monitors.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { ILivechatMonitor, IUser } from '@rocket.chat/core-typings';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
import type { PaginatedResult } from '@rocket.chat/rest-typings';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
|
||||
export async function findMonitors({
|
||||
text,
|
||||
pagination: { offset, count, sort },
|
||||
}: {
|
||||
text?: string;
|
||||
pagination: {
|
||||
offset: number;
|
||||
count: number;
|
||||
sort: {
|
||||
[key: string]: 1 | -1;
|
||||
};
|
||||
};
|
||||
}): Promise<PaginatedResult<{ monitors: ILivechatMonitor[] }>> {
|
||||
const query = {};
|
||||
if (text) {
|
||||
const filterReg = new RegExp(escapeRegExp(text), 'i');
|
||||
Object.assign(query, {
|
||||
$or: [{ username: filterReg }, { name: filterReg }, { 'emails.address': filterReg }],
|
||||
});
|
||||
}
|
||||
|
||||
const { cursor, totalCount } = Users.findPaginatedUsersInRolesWithQuery<ILivechatMonitor>('livechat-monitor', query, {
|
||||
sort: sort || { name: 1 },
|
||||
skip: offset,
|
||||
limit: count,
|
||||
projection: {
|
||||
username: 1,
|
||||
name: 1,
|
||||
status: 1,
|
||||
statusLivechat: 1,
|
||||
emails: 1,
|
||||
livechat: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const [monitors, total] = await Promise.all([cursor.toArray(), totalCount]);
|
||||
|
||||
return {
|
||||
monitors,
|
||||
count: monitors.length,
|
||||
offset,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export async function findMonitorByUsername({ username }: { username: string }): Promise<IUser> {
|
||||
const user = await Users.findOne(
|
||||
{ username, roles: 'livechat-monitor' },
|
||||
{
|
||||
projection: {
|
||||
username: 1,
|
||||
name: 1,
|
||||
status: 1,
|
||||
statusLivechat: 1,
|
||||
emails: 1,
|
||||
livechat: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('invalid-user');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
73
apps/meteor/ee/server/api/v1/omnichannel/lib/outbound.ts
Normal file
73
apps/meteor/ee/server/api/v1/omnichannel/lib/outbound.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Apps } from '@rocket.chat/apps';
|
||||
import type {
|
||||
IOutboundProvider,
|
||||
ValidOutboundProvider,
|
||||
IOutboundMessageProviderService,
|
||||
IOutboundProviderMetadata,
|
||||
IOutboundMessage,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import { ValidOutboundProviderList } from '@rocket.chat/core-typings';
|
||||
|
||||
import { OutboundMessageProvider } from '../../../../../../server/lib/OutboundMessageProvider';
|
||||
import { getOutboundService } from '../../../../../../server/lib/omnichannel/outboundcommunication';
|
||||
|
||||
export class OutboundMessageProviderService implements IOutboundMessageProviderService {
|
||||
private readonly provider: OutboundMessageProvider;
|
||||
|
||||
constructor() {
|
||||
this.provider = new OutboundMessageProvider();
|
||||
}
|
||||
|
||||
get outboundMessageProvider() {
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
private isProviderValid(type: any): type is ValidOutboundProvider {
|
||||
return ValidOutboundProviderList.includes(type);
|
||||
}
|
||||
|
||||
public listOutboundProviders(type?: string): IOutboundProvider[] {
|
||||
if (type !== undefined && !this.isProviderValid(type)) {
|
||||
throw new Error('Invalid type');
|
||||
}
|
||||
|
||||
return this.provider.getOutboundMessageProviders(type);
|
||||
}
|
||||
|
||||
public getProviderMetadata(providerId: string): Promise<IOutboundProviderMetadata> {
|
||||
const provider = this.provider.findOneByProviderId(providerId);
|
||||
if (!provider) {
|
||||
throw new Error('error-invalid-provider');
|
||||
}
|
||||
|
||||
return this.getProviderManager().getProviderMetadata(provider.appId, provider.type);
|
||||
}
|
||||
|
||||
private getProviderManager() {
|
||||
if (!Apps.self?.isLoaded()) {
|
||||
throw new Error('apps-engine-not-loaded');
|
||||
}
|
||||
|
||||
const manager = Apps.self?.getManager()?.getOutboundCommunicationProviderManager();
|
||||
if (!manager) {
|
||||
throw new Error('apps-engine-not-configured-correctly');
|
||||
}
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
public sendMessage(providerId: string, message: IOutboundMessage) {
|
||||
const provider = this.provider.findOneByProviderId(providerId);
|
||||
if (!provider) {
|
||||
throw new Error('error-invalid-provider');
|
||||
}
|
||||
|
||||
return this.getProviderManager().sendOutboundMessage(provider.appId, provider.type, message);
|
||||
}
|
||||
}
|
||||
|
||||
export const outboundMessageProvider = new OutboundMessageProviderService();
|
||||
|
||||
getOutboundService.patch(() => {
|
||||
return outboundMessageProvider;
|
||||
});
|
||||
120
apps/meteor/ee/server/api/v1/omnichannel/lib/priorities.ts
Normal file
120
apps/meteor/ee/server/api/v1/omnichannel/lib/priorities.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Message } from '@rocket.chat/core-services';
|
||||
import type { ILivechatPriority, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
|
||||
import { LivechatInquiry, LivechatPriority, LivechatRooms } from '@rocket.chat/models';
|
||||
import type { PaginatedResult } from '@rocket.chat/rest-typings';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
import type { FindOptions } from 'mongodb';
|
||||
|
||||
import { notifyOnLivechatInquiryChangedByRoom, notifyOnRoomChanged } from '../../../../../../server/lib/notifyListener';
|
||||
import { logger } from '../../../../lib/omnichannel/logger';
|
||||
|
||||
type FindPriorityParams = {
|
||||
text?: string;
|
||||
pagination: {
|
||||
offset: number;
|
||||
count: number;
|
||||
sort: FindOptions<ILivechatPriority>['sort'];
|
||||
};
|
||||
};
|
||||
|
||||
export async function findPriority({
|
||||
text,
|
||||
pagination: { offset, count, sort },
|
||||
}: FindPriorityParams): Promise<PaginatedResult<{ priorities: ILivechatPriority[] }>> {
|
||||
const query = {
|
||||
...(text && { $or: [{ name: new RegExp(escapeRegExp(text), 'i') }, { description: new RegExp(escapeRegExp(text), 'i') }] }),
|
||||
};
|
||||
|
||||
const { cursor, totalCount } = LivechatPriority.findPaginated(query, {
|
||||
sort: sort || { name: 1 },
|
||||
skip: offset,
|
||||
limit: count,
|
||||
});
|
||||
|
||||
const [priorities, total] = await Promise.all([cursor.toArray(), totalCount]);
|
||||
|
||||
return {
|
||||
priorities,
|
||||
count: priorities.length,
|
||||
offset,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export async function updatePriority(_id: string, data: Pick<ILivechatPriority, 'name'> & { reset?: boolean }): Promise<ILivechatPriority> {
|
||||
if (data.name) {
|
||||
// If we want to enforce translated duplicates we need to change this
|
||||
const priority = await LivechatPriority.findOneNameUsingRegex(data.name, { projection: { name: 1 } });
|
||||
if (priority && priority._id !== _id) {
|
||||
throw new Error('error-duplicate-priority-name');
|
||||
}
|
||||
}
|
||||
|
||||
const createdResult = await LivechatPriority.updatePriority(_id, data.reset || false, data.name);
|
||||
|
||||
if (!createdResult) {
|
||||
logger.error({ msg: 'Error updating priority: unsuccessful result from MongoDB', priorityId: _id, result: createdResult });
|
||||
throw Error('error-unable-to-update-priority');
|
||||
}
|
||||
|
||||
return createdResult;
|
||||
}
|
||||
|
||||
export const updateRoomPriority = async (
|
||||
rid: string,
|
||||
user: Required<Pick<IUser, '_id' | 'username' | 'name'>>,
|
||||
priorityId: string,
|
||||
): Promise<void> => {
|
||||
const room = await LivechatRooms.findOneById(rid);
|
||||
if (!room) {
|
||||
throw new Error('error-room-does-not-exist');
|
||||
}
|
||||
|
||||
const priority = await LivechatPriority.findOneById(priorityId);
|
||||
if (!priority) {
|
||||
throw new Error('error-invalid-priority');
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
LivechatRooms.setPriorityByRoomId(rid, priority),
|
||||
LivechatInquiry.setPriorityForRoom(rid, priority),
|
||||
addPriorityChangeHistoryToRoom(room._id, user, priority),
|
||||
]);
|
||||
|
||||
void notifyOnRoomChanged({ ...room, priorityId: priority._id, priorityWeight: priority.sortItem }, 'updated');
|
||||
void notifyOnLivechatInquiryChangedByRoom(rid, 'updated');
|
||||
};
|
||||
|
||||
export const removePriorityFromRoom = async (rid: string, user: Required<Pick<IUser, '_id' | 'username' | 'name'>>): Promise<void> => {
|
||||
const room = await LivechatRooms.findOneById<Omit<IOmnichannelRoom, 'priorityId' | 'priorityWeight'>>(rid, {
|
||||
projection: { priorityId: 0, priorityWeight: 0 },
|
||||
});
|
||||
if (!room) {
|
||||
throw new Error('error-room-does-not-exist');
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
LivechatRooms.unsetPriorityByRoomId(rid),
|
||||
LivechatInquiry.unsetPriorityForRoom(rid),
|
||||
addPriorityChangeHistoryToRoom(rid, user),
|
||||
]);
|
||||
|
||||
void notifyOnRoomChanged(room, 'updated');
|
||||
void notifyOnLivechatInquiryChangedByRoom(rid, 'updated');
|
||||
};
|
||||
|
||||
const addPriorityChangeHistoryToRoom = async (
|
||||
roomId: string,
|
||||
user: Required<Pick<IUser, '_id' | 'username' | 'name'>>,
|
||||
priority?: Pick<ILivechatPriority, 'name' | 'i18n'>,
|
||||
) => {
|
||||
await Message.saveSystemMessage('omnichannel_priority_change_history', roomId, '', user, {
|
||||
priorityData: {
|
||||
definedBy: {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
},
|
||||
...(priority && { priority }),
|
||||
},
|
||||
});
|
||||
};
|
||||
58
apps/meteor/ee/server/api/v1/omnichannel/lib/sla.ts
Normal file
58
apps/meteor/ee/server/api/v1/omnichannel/lib/sla.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { IOmnichannelServiceLevelAgreements, IUser } from '@rocket.chat/core-typings';
|
||||
import { OmnichannelServiceLevelAgreements } from '@rocket.chat/models';
|
||||
import type { PaginatedResult } from '@rocket.chat/rest-typings';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
import type { FindOptions } from 'mongodb';
|
||||
|
||||
import {
|
||||
addSlaChangeHistoryToRoom,
|
||||
removeInquiryQueueSla,
|
||||
removeSlaFromRoom,
|
||||
updateInquiryQueueSla,
|
||||
updateRoomSlaWeights,
|
||||
} from '../../../../lib/omnichannel/SlaHelper';
|
||||
|
||||
type FindSLAParams = {
|
||||
text?: string;
|
||||
pagination: {
|
||||
offset: number;
|
||||
count: number;
|
||||
sort: FindOptions<IOmnichannelServiceLevelAgreements>['sort'];
|
||||
};
|
||||
};
|
||||
|
||||
export async function findSLA({
|
||||
text,
|
||||
pagination: { offset, count, sort },
|
||||
}: FindSLAParams): Promise<PaginatedResult<{ sla: IOmnichannelServiceLevelAgreements[] }>> {
|
||||
const query = {
|
||||
...(text && { $or: [{ name: new RegExp(escapeRegExp(text), 'i') }, { description: new RegExp(escapeRegExp(text), 'i') }] }),
|
||||
};
|
||||
|
||||
const { cursor, totalCount } = OmnichannelServiceLevelAgreements.findPaginated(query, {
|
||||
sort: sort || { name: 1 },
|
||||
skip: offset,
|
||||
limit: count,
|
||||
});
|
||||
|
||||
const [sla, total] = await Promise.all([cursor.toArray(), totalCount]);
|
||||
|
||||
return {
|
||||
sla,
|
||||
count: sla.length,
|
||||
offset,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export const updateRoomSLA = async (
|
||||
roomId: string,
|
||||
user: Required<Pick<IUser, '_id' | 'username' | 'name'>>,
|
||||
sla: Pick<IOmnichannelServiceLevelAgreements, '_id' | 'name' | 'dueTimeInMinutes'>,
|
||||
) => {
|
||||
await Promise.all([updateInquiryQueueSla(roomId, sla), updateRoomSlaWeights(roomId, sla), addSlaChangeHistoryToRoom(roomId, user, sla)]);
|
||||
};
|
||||
|
||||
export const removeRoomSLA = async (roomId: string, user: Required<Pick<IUser, '_id' | 'username' | 'name'>>) => {
|
||||
await Promise.all([removeInquiryQueueSla(roomId), removeSlaFromRoom(roomId), addSlaChangeHistoryToRoom(roomId, user)]);
|
||||
};
|
||||
127
apps/meteor/ee/server/api/v1/omnichannel/lib/tags.ts
Normal file
127
apps/meteor/ee/server/api/v1/omnichannel/lib/tags.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { ILivechatTag, FindTagsResult } from '@rocket.chat/core-typings';
|
||||
import { LivechatTag } from '@rocket.chat/models';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
import type { Filter, FindOptions } from 'mongodb';
|
||||
|
||||
import { getDepartmentsWhichUserCanAccess } from './departments';
|
||||
import { hasPermissionAsync } from '../../../../../../server/lib/authorization/hasPermission';
|
||||
import { helperLogger } from '../../../../lib/omnichannel/logger';
|
||||
|
||||
type FindTagsParams = {
|
||||
userId: string;
|
||||
text?: string;
|
||||
pagination: {
|
||||
offset: number;
|
||||
count: number;
|
||||
sort: FindOptions<ILivechatTag>['sort'];
|
||||
};
|
||||
department?: string;
|
||||
viewAll?: boolean;
|
||||
};
|
||||
|
||||
type FindTagsByIdParams = {
|
||||
userId: string;
|
||||
tagId: string;
|
||||
};
|
||||
|
||||
type FindTagsByIdResult = ILivechatTag | null;
|
||||
|
||||
// If viewAll is true
|
||||
// -> & user has access to all tags
|
||||
// -> then all tags will be returned
|
||||
// -> Pages:
|
||||
// - Admin > Omnichannel > Tags
|
||||
// - Current chat's panel, filter by tags
|
||||
// - Canned response creation
|
||||
// -> & user does not have access to all tags
|
||||
// -> then only public tags will be returned (unauthorized access - no page uses this)
|
||||
// If viewAll is false
|
||||
// -> & user has access to all tags
|
||||
// -> & department is not specified
|
||||
// -> only public tags will be returned (Pages: Close chat modal tag selection for chats not associated with a department being closed by manager)
|
||||
// -> & department is specified
|
||||
// -> only tags associated with the department will be returned (Page: Close chat modal tag selection for chats associated with a department being closed by manager)
|
||||
// -> & user does not have access to all tags (same as viewAll = false & user has access to all tags)
|
||||
// -> & department is not specified
|
||||
// -> only public tags will be returned (Page: Close chat modal tag selection for chats not associated with a department being closed by agent)
|
||||
// -> & department is specified
|
||||
// -> only tags associated with the department will be returned (Page: Close chat modal tag selection for chats associated with a department being closed by agent)
|
||||
export async function findTags({
|
||||
userId,
|
||||
text,
|
||||
department,
|
||||
viewAll,
|
||||
pagination: { offset, count, sort },
|
||||
}: FindTagsParams): Promise<FindTagsResult> {
|
||||
let filteredDepartmentIds: string[] = [];
|
||||
|
||||
if (!(await hasPermissionAsync(userId, 'manage-livechat-tags'))) {
|
||||
if (viewAll) {
|
||||
viewAll = false;
|
||||
}
|
||||
|
||||
// Get a list of all departments this user has access to and only
|
||||
// return tags that are associated with those departments
|
||||
filteredDepartmentIds = await getDepartmentsWhichUserCanAccess(userId);
|
||||
|
||||
helperLogger.debug({
|
||||
msg: 'User does not have permission to manage livechat tags. Filtering tags by departments user has access to.',
|
||||
userId,
|
||||
accessibleDepartmentsLength: filteredDepartmentIds.length,
|
||||
top5AccessibleDepartments: filteredDepartmentIds.slice(0, 5),
|
||||
});
|
||||
|
||||
if (department && !filteredDepartmentIds.includes(department)) {
|
||||
department = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (department) {
|
||||
// In certain cases, the user would only want to see tags for a specific department
|
||||
// EG: When closing the chat which is associated with a specific department
|
||||
// user get's to choose which tag to use to close the chat
|
||||
// (only tags associated with the department should be shown)
|
||||
filteredDepartmentIds = [department];
|
||||
}
|
||||
|
||||
const query: {
|
||||
$and?: Filter<ILivechatTag>[];
|
||||
} = {
|
||||
$and: [
|
||||
...(text ? [{ $or: [{ name: new RegExp(escapeRegExp(text), 'i') }, { description: new RegExp(escapeRegExp(text), 'i') }] }] : []),
|
||||
...(!viewAll
|
||||
? [
|
||||
{
|
||||
$or: [
|
||||
{ departments: { $size: 0 } },
|
||||
...(filteredDepartmentIds.length ? [{ departments: { $in: filteredDepartmentIds } }] : []),
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
|
||||
if (!query?.$and?.length) {
|
||||
delete query.$and;
|
||||
}
|
||||
|
||||
const { cursor, totalCount } = LivechatTag.findPaginated(query, {
|
||||
sort: sort || { name: 1 },
|
||||
skip: offset,
|
||||
limit: count,
|
||||
});
|
||||
|
||||
const [tags, total] = await Promise.all([cursor.toArray(), totalCount]);
|
||||
|
||||
return {
|
||||
tags,
|
||||
count: tags.length,
|
||||
offset,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export async function findTagById({ tagId }: FindTagsByIdParams): Promise<FindTagsByIdResult> {
|
||||
return LivechatTag.findOneById(tagId);
|
||||
}
|
||||
57
apps/meteor/ee/server/api/v1/omnichannel/lib/triggers.ts
Normal file
57
apps/meteor/ee/server/api/v1/omnichannel/lib/triggers.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
|
||||
|
||||
export async function callTriggerExternalService({
|
||||
url,
|
||||
timeout,
|
||||
fallbackMessage,
|
||||
body,
|
||||
headers,
|
||||
}: {
|
||||
url: string;
|
||||
timeout: number;
|
||||
fallbackMessage: string;
|
||||
body: Record<string, any>;
|
||||
headers: Record<string, string>;
|
||||
}) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
timeout: timeout || 1000,
|
||||
body,
|
||||
headers,
|
||||
method: 'POST',
|
||||
// SECURITY: Integrations can only be configured by users with enough privileges. It's ok to disable this check here.
|
||||
ignoreSsrfValidation: true,
|
||||
});
|
||||
|
||||
if (!response.ok || response.status !== 200) {
|
||||
const text = await response.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const { contents } = data;
|
||||
|
||||
if (
|
||||
!Array.isArray(contents) ||
|
||||
!contents.length ||
|
||||
!contents.every(({ msg, order }) => typeof msg === 'string' && typeof order === 'number')
|
||||
) {
|
||||
throw new Error('External service response does not match expected format');
|
||||
}
|
||||
|
||||
return {
|
||||
response: {
|
||||
statusCode: response.status,
|
||||
contents: data?.contents || [],
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
const isTimeout = error.message === 'The user aborted a request.';
|
||||
return {
|
||||
error: isTimeout ? 'error-timeout' : 'error-invalid-external-service-response',
|
||||
response: error.message,
|
||||
fallbackMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
92
apps/meteor/ee/server/api/v1/omnichannel/lib/units.ts
Normal file
92
apps/meteor/ee/server/api/v1/omnichannel/lib/units.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { IOmnichannelBusinessUnit, ILivechatUnitMonitor } from '@rocket.chat/core-typings';
|
||||
import { LivechatUnitMonitors, LivechatUnit } from '@rocket.chat/models';
|
||||
import { getUnitsFromUser } from '@rocket.chat/omni-core-ee';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
import type { FindOptions } from 'mongodb';
|
||||
|
||||
export async function findUnitsOfUser({
|
||||
text,
|
||||
userId,
|
||||
pagination: { offset, count, sort },
|
||||
}: {
|
||||
text?: string;
|
||||
userId: string;
|
||||
pagination: {
|
||||
offset: number;
|
||||
count: number;
|
||||
sort: FindOptions<IOmnichannelBusinessUnit>['sort'];
|
||||
};
|
||||
}): Promise<{
|
||||
units: IOmnichannelBusinessUnit[];
|
||||
count: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
}> {
|
||||
const filter = text && new RegExp(escapeRegExp(text), 'i');
|
||||
|
||||
// TODO: an aggregation woudl be faster? or nah?
|
||||
const myUnits = await LivechatUnitMonitors.findByMonitorId(userId)
|
||||
.map((u) => u.unitId)
|
||||
.toArray();
|
||||
const query = { _id: { $in: myUnits }, ...(text && { $or: [{ name: filter }] }) };
|
||||
|
||||
const { cursor, totalCount } = LivechatUnit.findPaginatedUnits(query, {
|
||||
sort: sort || { name: 1 },
|
||||
skip: offset,
|
||||
limit: count,
|
||||
});
|
||||
|
||||
const [units, total] = await Promise.all([cursor.toArray(), totalCount]);
|
||||
|
||||
return {
|
||||
units,
|
||||
count: units.length,
|
||||
offset,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export async function findUnits({
|
||||
text,
|
||||
pagination: { offset, count, sort },
|
||||
}: {
|
||||
text?: string;
|
||||
pagination: {
|
||||
offset: number;
|
||||
count: number;
|
||||
sort: FindOptions<IOmnichannelBusinessUnit>['sort'];
|
||||
};
|
||||
}): Promise<{
|
||||
units: IOmnichannelBusinessUnit[];
|
||||
count: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
}> {
|
||||
const filter = text && new RegExp(escapeRegExp(text), 'i');
|
||||
|
||||
const query = { ...(text && { $or: [{ name: filter }] }) };
|
||||
|
||||
const { cursor, totalCount } = LivechatUnit.findPaginatedUnits(query, {
|
||||
sort: sort || { name: 1 },
|
||||
skip: offset,
|
||||
limit: count,
|
||||
});
|
||||
|
||||
const [units, total] = await Promise.all([cursor.toArray(), totalCount]);
|
||||
|
||||
return {
|
||||
units,
|
||||
count: units.length,
|
||||
offset,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
export async function findUnitMonitors({ unitId }: { unitId: string }): Promise<ILivechatUnitMonitor[]> {
|
||||
return LivechatUnitMonitors.find({ unitId }).toArray();
|
||||
}
|
||||
|
||||
export async function findUnitById({ unitId, userId }: { unitId: string; userId: string }): Promise<IOmnichannelBusinessUnit | null> {
|
||||
const unitsFromUser = await getUnitsFromUser(userId);
|
||||
return LivechatUnit.findOneById(unitId, {}, { unitsFromUser });
|
||||
}
|
||||
128
apps/meteor/ee/server/api/v1/omnichannel/monitors.ts
Normal file
128
apps/meteor/ee/server/api/v1/omnichannel/monitors.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { ILivechatMonitor } from '@rocket.chat/core-typings';
|
||||
import {
|
||||
isPOSTLivechatMonitorCreateRequest,
|
||||
isPOSTLivechatMonitorsDeleteRequest,
|
||||
POSTLivechatMonitorsCreateSuccessResponse,
|
||||
POSTLivechatMonitorsDeleteSuccessResponse,
|
||||
validateBadRequestErrorResponse,
|
||||
validateForbiddenErrorResponse,
|
||||
validateUnauthorizedErrorResponse,
|
||||
} from '@rocket.chat/rest-typings';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { findMonitors, findMonitorByUsername } from './lib/monitors';
|
||||
import { API } from '../../../../../server/api';
|
||||
import type { ExtractRoutesFromAPI } from '../../../../../server/api/ApiClass';
|
||||
import { getPaginationItems } from '../../../../../server/api/lib/getPaginationItems';
|
||||
import { LivechatEnterprise } from '../../../lib/omnichannel/LivechatEnterprise';
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/monitors',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['manage-livechat-monitors'],
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
const { text } = this.queryParams;
|
||||
|
||||
return API.v1.success(
|
||||
await findMonitors({
|
||||
text,
|
||||
pagination: {
|
||||
offset,
|
||||
count,
|
||||
sort,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/monitors/:username',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['manage-livechat-monitors'],
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { username } = this.urlParams;
|
||||
|
||||
return API.v1.success(
|
||||
(await findMonitorByUsername({
|
||||
username,
|
||||
})) as unknown as ILivechatMonitor,
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const livechatMonitorsEndpoints = API.v1
|
||||
.post(
|
||||
'livechat/monitors.create',
|
||||
{
|
||||
response: {
|
||||
200: POSTLivechatMonitorsCreateSuccessResponse,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
authRequired: true,
|
||||
permissionsRequired: ['manage-livechat-monitors'],
|
||||
license: ['livechat-enterprise'],
|
||||
body: isPOSTLivechatMonitorCreateRequest,
|
||||
},
|
||||
async function action() {
|
||||
const { username } = this.bodyParams;
|
||||
|
||||
const result = await LivechatEnterprise.addMonitor(username);
|
||||
|
||||
return API.v1.success(result);
|
||||
},
|
||||
)
|
||||
.post(
|
||||
'livechat/monitors.delete',
|
||||
{
|
||||
response: {
|
||||
200: POSTLivechatMonitorsDeleteSuccessResponse,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
authRequired: true,
|
||||
permissionsRequired: ['manage-livechat-monitors'],
|
||||
license: ['livechat-enterprise'],
|
||||
body: isPOSTLivechatMonitorsDeleteRequest,
|
||||
},
|
||||
async function action() {
|
||||
const { username } = this.bodyParams;
|
||||
|
||||
try {
|
||||
const result = await LivechatEnterprise.removeMonitor(username);
|
||||
if (!result) {
|
||||
return API.v1.failure('error-removing-monitor');
|
||||
}
|
||||
|
||||
return API.v1.success();
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Meteor.Error) {
|
||||
return API.v1.failure(error.reason);
|
||||
}
|
||||
|
||||
return API.v1.failure('error-removing-monitor');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
type LivechatMonitorsEndpoints = ExtractRoutesFromAPI<typeof livechatMonitorsEndpoints>;
|
||||
|
||||
declare module '@rocket.chat/rest-typings' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
|
||||
interface Endpoints extends LivechatMonitorsEndpoints {}
|
||||
}
|
||||
91
apps/meteor/ee/server/api/v1/omnichannel/outbound.ts
Normal file
91
apps/meteor/ee/server/api/v1/omnichannel/outbound.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { canSendOutboundMessage } from '@rocket.chat/omni-core-ee';
|
||||
import {
|
||||
validateBadRequestErrorResponse,
|
||||
validateForbiddenErrorResponse,
|
||||
validateUnauthorizedErrorResponse,
|
||||
} from '@rocket.chat/rest-typings';
|
||||
|
||||
import { outboundMessageProvider } from './lib/outbound';
|
||||
import { API } from '../../../../../server/api';
|
||||
import type { ExtractRoutesFromAPI } from '../../../../../server/api/ApiClass';
|
||||
import {
|
||||
GETOutboundProvidersResponseSchema,
|
||||
GETOutboundProviderParamsSchema,
|
||||
GETOutboundProviderBadRequestErrorSchema,
|
||||
GETOutboundProviderMetadataSchema,
|
||||
POSTOutboundMessageParams,
|
||||
POSTOutboundMessageSuccessSchema,
|
||||
} from '../../../lib/omnichannel/outboundcomms/rest';
|
||||
|
||||
const outboundCommsEndpoints = API.v1
|
||||
.get(
|
||||
'omnichannel/outbound/providers',
|
||||
{
|
||||
response: {
|
||||
200: GETOutboundProvidersResponseSchema,
|
||||
400: GETOutboundProviderBadRequestErrorSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
query: GETOutboundProviderParamsSchema,
|
||||
permissionsRequired: ['outbound.send-messages'],
|
||||
authRequired: true,
|
||||
license: ['outbound-messaging'],
|
||||
},
|
||||
async function action() {
|
||||
const { type } = this.queryParams;
|
||||
|
||||
const providers = outboundMessageProvider.listOutboundProviders(type);
|
||||
return API.v1.success({
|
||||
providers,
|
||||
});
|
||||
},
|
||||
)
|
||||
.get(
|
||||
'omnichannel/outbound/providers/:id/metadata',
|
||||
{
|
||||
response: {
|
||||
200: GETOutboundProviderMetadataSchema,
|
||||
400: GETOutboundProviderBadRequestErrorSchema,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
permissionsRequired: ['outbound.send-messages'],
|
||||
authRequired: true,
|
||||
license: ['outbound-messaging'],
|
||||
},
|
||||
async function action() {
|
||||
const { id } = this.urlParams;
|
||||
|
||||
const providerMetadata = await outboundMessageProvider.getProviderMetadata(id);
|
||||
return API.v1.success({
|
||||
metadata: providerMetadata,
|
||||
});
|
||||
},
|
||||
)
|
||||
.post(
|
||||
'omnichannel/outbound/providers/:id/message',
|
||||
{
|
||||
response: {
|
||||
200: POSTOutboundMessageSuccessSchema,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
authRequired: true,
|
||||
permissionsRequired: ['outbound.send-messages'],
|
||||
body: POSTOutboundMessageParams,
|
||||
license: ['outbound-messaging'],
|
||||
},
|
||||
async function action() {
|
||||
const { id } = this.urlParams;
|
||||
const { departmentId, agentId } = this.bodyParams;
|
||||
|
||||
await canSendOutboundMessage(this.userId, agentId, departmentId);
|
||||
|
||||
await outboundMessageProvider.sendMessage(id, this.bodyParams);
|
||||
return API.v1.success();
|
||||
},
|
||||
);
|
||||
|
||||
export type OutboundCommsEndpoints = ExtractRoutesFromAPI<typeof outboundCommsEndpoints>;
|
||||
99
apps/meteor/ee/server/api/v1/omnichannel/priorities.ts
Normal file
99
apps/meteor/ee/server/api/v1/omnichannel/priorities.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { LivechatPriority } from '@rocket.chat/models';
|
||||
import { isGETLivechatPrioritiesParams, isPUTLivechatPriority } from '@rocket.chat/rest-typings';
|
||||
|
||||
import { findPriority, updatePriority } from './lib/priorities';
|
||||
import { API } from '../../../../../server/api';
|
||||
import { getPaginationItems } from '../../../../../server/api/lib/getPaginationItems';
|
||||
import { notifyOnLivechatPriorityChanged } from '../../../../../server/lib/notifyListener';
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/priorities',
|
||||
{
|
||||
authRequired: true,
|
||||
validateParams: isGETLivechatPrioritiesParams,
|
||||
permissionsRequired: { GET: { permissions: ['manage-livechat-priorities', 'view-l-room'], operation: 'hasAny' } },
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
const { text } = this.queryParams;
|
||||
|
||||
return API.v1.success(
|
||||
await findPriority({
|
||||
text,
|
||||
pagination: {
|
||||
offset,
|
||||
count,
|
||||
sort,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/priorities/:priorityId',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: {
|
||||
GET: { permissions: ['manage-livechat-priorities', 'view-l-room'], operation: 'hasAny' },
|
||||
PUT: { permissions: ['manage-livechat-priorities'], operation: 'hasAny' },
|
||||
},
|
||||
validateParams: { PUT: isPUTLivechatPriority },
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { priorityId } = this.urlParams;
|
||||
const priority = await LivechatPriority.findOneById(priorityId);
|
||||
|
||||
if (!priority) {
|
||||
return API.v1.notFound(`Priority with id ${priorityId} not found`);
|
||||
}
|
||||
|
||||
return API.v1.success(priority);
|
||||
},
|
||||
async put() {
|
||||
const { priorityId } = this.urlParams;
|
||||
|
||||
await updatePriority(priorityId, this.bodyParams);
|
||||
|
||||
void notifyOnLivechatPriorityChanged({ _id: priorityId, ...this.bodyParams });
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/priorities.reset',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: {
|
||||
POST: { permissions: ['manage-livechat-priorities'], operation: 'hasAny' },
|
||||
GET: { permissions: ['manage-livechat-priorities'], operation: 'hasAny' },
|
||||
},
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async post() {
|
||||
if (!(await LivechatPriority.canResetPriorities())) {
|
||||
return API.v1.failure();
|
||||
}
|
||||
|
||||
const eligiblePriorities = (await LivechatPriority.findByDirty().toArray()).map(({ _id }) => _id);
|
||||
|
||||
await LivechatPriority.resetPriorities(eligiblePriorities);
|
||||
|
||||
eligiblePriorities.forEach((_id) => notifyOnLivechatPriorityChanged({ _id, name: undefined }));
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
async get() {
|
||||
return API.v1.success({ reset: await LivechatPriority.canResetPriorities() });
|
||||
},
|
||||
},
|
||||
);
|
||||
155
apps/meteor/ee/server/api/v1/omnichannel/reports.ts
Normal file
155
apps/meteor/ee/server/api/v1/omnichannel/reports.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { isGETDashboardConversationsByType } from '@rocket.chat/rest-typings';
|
||||
import type { Moment } from 'moment';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
findAllConversationsBySourceCached,
|
||||
findAllConversationsByStatusCached,
|
||||
findAllConversationsByDepartmentCached,
|
||||
findAllConversationsByTagsCached,
|
||||
findAllConversationsByAgentsCached,
|
||||
} from './lib/dashboards';
|
||||
import { API } from '../../../../../server/api';
|
||||
import { restrictQuery } from '../../../lib/omnichannel/restrictQuery';
|
||||
|
||||
const checkDates = (start: Moment, end: Moment) => {
|
||||
if (!start.isValid()) {
|
||||
throw new Error('The "start" query parameter must be a valid date.');
|
||||
}
|
||||
if (!end.isValid()) {
|
||||
throw new Error('The "end" query parameter must be a valid date.');
|
||||
}
|
||||
// Check dates are no more than 1 year apart using moment
|
||||
// 1.01 === "we allow to pass year by some hours/days"
|
||||
if (moment(end).startOf('day').diff(moment(start).startOf('day'), 'year', true) > 1.01) {
|
||||
throw new Error('The "start" and "end" query parameters must be less than 1 year apart.');
|
||||
}
|
||||
|
||||
if (start.isAfter(end)) {
|
||||
throw new Error('The "start" query parameter must be before the "end" query parameter.');
|
||||
}
|
||||
};
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/dashboards/conversations-by-source',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-reports'],
|
||||
validateParams: isGETDashboardConversationsByType,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { start, end } = this.queryParams;
|
||||
|
||||
const startDate = moment(start);
|
||||
const endDate = moment(end);
|
||||
|
||||
checkDates(startDate, endDate);
|
||||
|
||||
const extraQuery = await restrictQuery({ userId: this.userId });
|
||||
const result = await findAllConversationsBySourceCached({ start: startDate.toDate(), end: endDate.toDate(), extraQuery });
|
||||
|
||||
return API.v1.success(result);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/dashboards/conversations-by-status',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-reports'],
|
||||
validateParams: isGETDashboardConversationsByType,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { start, end } = this.queryParams;
|
||||
|
||||
const startDate = moment(start);
|
||||
const endDate = moment(end);
|
||||
|
||||
checkDates(startDate, endDate);
|
||||
const extraQuery = await restrictQuery({ userId: this.userId });
|
||||
const result = await findAllConversationsByStatusCached({ start: startDate.toDate(), end: endDate.toDate(), extraQuery });
|
||||
|
||||
return API.v1.success(result);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/dashboards/conversations-by-department',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-reports'],
|
||||
validateParams: isGETDashboardConversationsByType,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { start, end } = this.queryParams;
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
|
||||
const startDate = moment(start);
|
||||
const endDate = moment(end);
|
||||
|
||||
checkDates(startDate, endDate);
|
||||
const extraQuery = await restrictQuery({ userId: this.userId });
|
||||
const result = await findAllConversationsByDepartmentCached({ start: startDate.toDate(), end: endDate.toDate(), sort, extraQuery });
|
||||
|
||||
return API.v1.success(result);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/dashboards/conversations-by-tags',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-reports'],
|
||||
validateParams: isGETDashboardConversationsByType,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { start, end } = this.queryParams;
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
|
||||
const startDate = moment(start);
|
||||
const endDate = moment(end);
|
||||
|
||||
checkDates(startDate, endDate);
|
||||
const extraQuery = await restrictQuery({ userId: this.userId });
|
||||
const result = await findAllConversationsByTagsCached({ start: startDate.toDate(), end: endDate.toDate(), sort, extraQuery });
|
||||
|
||||
return API.v1.success(result);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/analytics/dashboards/conversations-by-agent',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-reports'],
|
||||
validateParams: isGETDashboardConversationsByType,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { start, end } = this.queryParams;
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
|
||||
const startDate = moment(start);
|
||||
const endDate = moment(end);
|
||||
|
||||
checkDates(startDate, endDate);
|
||||
const extraQuery = await restrictQuery({ userId: this.userId });
|
||||
const result = await findAllConversationsByAgentsCached({ start: startDate.toDate(), end: endDate.toDate(), sort, extraQuery });
|
||||
|
||||
return API.v1.success(result);
|
||||
},
|
||||
},
|
||||
);
|
||||
139
apps/meteor/ee/server/api/v1/omnichannel/rooms.ts
Normal file
139
apps/meteor/ee/server/api/v1/omnichannel/rooms.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { OmnichannelEEService } from '@rocket.chat/core-services';
|
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
|
||||
import { LivechatRooms, Subscriptions } from '@rocket.chat/models';
|
||||
import { isLivechatRoomOnHoldProps, isLivechatRoomResumeOnHoldProps, isPOSTLivechatRoomPriorityParams } from '@rocket.chat/rest-typings';
|
||||
|
||||
import { removePriorityFromRoom, updateRoomPriority } from './lib/priorities';
|
||||
import { API } from '../../../../../server/api';
|
||||
import { hasPermissionAsync } from '../../../../../server/lib/authorization/hasPermission';
|
||||
import { i18n } from '../../../../../server/lib/i18n';
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/room.onHold',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['on-hold-livechat-room'],
|
||||
validateParams: isLivechatRoomOnHoldProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async post() {
|
||||
const { roomId } = this.bodyParams;
|
||||
|
||||
type Room = Pick<IOmnichannelRoom, '_id' | 't' | 'open' | 'onHold' | 'u' | 'lastMessage' | 'servedBy'>;
|
||||
|
||||
const room = await LivechatRooms.findOneById<Room>(roomId, {
|
||||
projection: { _id: 1, t: 1, open: 1, onHold: 1, u: 1, lastMessage: 1, servedBy: 1 },
|
||||
});
|
||||
if (!room) {
|
||||
throw new Error('error-invalid-room');
|
||||
}
|
||||
|
||||
const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId, { projection: { _id: 1 } });
|
||||
if (!subscription && !(await hasPermissionAsync(this.user, 'on-hold-others-livechat-room'))) {
|
||||
throw new Error('Not_authorized');
|
||||
}
|
||||
|
||||
const onHoldBy = { _id: this.userId, username: this.user.username, name: this.user.name };
|
||||
const comment = i18n.t('Omnichannel_On_Hold_manually', {
|
||||
user: onHoldBy.name || `@${onHoldBy.username}`,
|
||||
});
|
||||
|
||||
await OmnichannelEEService.placeRoomOnHold(room, comment, this.user);
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/room.resumeOnHold',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-l-room'],
|
||||
validateParams: isLivechatRoomResumeOnHoldProps,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async post() {
|
||||
const { roomId } = this.bodyParams;
|
||||
if (!roomId || roomId.trim() === '') {
|
||||
throw new Error('invalid-param');
|
||||
}
|
||||
|
||||
type Room = Pick<IOmnichannelRoom, '_id' | 't' | 'open' | 'onHold' | 'servedBy' | 'u' | 'lastMessage'>;
|
||||
|
||||
const room = await LivechatRooms.findOneById<Room>(roomId, {
|
||||
projection: { t: 1, open: 1, onHold: 1, servedBy: 1 },
|
||||
});
|
||||
if (!room) {
|
||||
throw new Error('error-invalid-room');
|
||||
}
|
||||
|
||||
const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId, { projection: { _id: 1 } });
|
||||
if (!subscription && !(await hasPermissionAsync(this.user, 'on-hold-others-livechat-room'))) {
|
||||
throw new Error('Not_authorized');
|
||||
}
|
||||
|
||||
const { name, username, _id: userId } = this.user;
|
||||
const onHoldBy = { _id: userId, username, name };
|
||||
const comment = i18n.t('Omnichannel_on_hold_chat_resumed_manually', {
|
||||
user: onHoldBy.name || `@${onHoldBy.username}`,
|
||||
});
|
||||
|
||||
await OmnichannelEEService.resumeRoomOnHold(room, comment, this.user, true);
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/room/:rid/priority',
|
||||
{
|
||||
authRequired: true,
|
||||
validateParams: { POST: isPOSTLivechatRoomPriorityParams },
|
||||
permissionsRequired: {
|
||||
POST: { permissions: ['view-l-room'], operation: 'hasAny' },
|
||||
DELETE: { permissions: ['view-l-room'], operation: 'hasAny' },
|
||||
},
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async post() {
|
||||
const { rid } = this.urlParams;
|
||||
const { priorityId } = this.bodyParams;
|
||||
|
||||
if (!this.user.username) {
|
||||
return API.v1.failure('Invalid user');
|
||||
}
|
||||
|
||||
await updateRoomPriority(
|
||||
rid,
|
||||
{
|
||||
_id: this.user._id,
|
||||
name: this.user.name || '',
|
||||
username: this.user.username,
|
||||
},
|
||||
priorityId,
|
||||
);
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
async delete() {
|
||||
const { rid } = this.urlParams;
|
||||
|
||||
if (!this.user.username) {
|
||||
return API.v1.failure('Invalid user');
|
||||
}
|
||||
|
||||
await removePriorityFromRoom(rid, {
|
||||
_id: this.user._id,
|
||||
name: this.user.name || '',
|
||||
username: this.user.username,
|
||||
});
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
},
|
||||
);
|
||||
107
apps/meteor/ee/server/api/v1/omnichannel/sla.ts
Normal file
107
apps/meteor/ee/server/api/v1/omnichannel/sla.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { OmnichannelServiceLevelAgreements } from '@rocket.chat/models';
|
||||
import { isLivechatPrioritiesProps, isCreateOrUpdateLivechatSlaProps } from '@rocket.chat/rest-typings';
|
||||
|
||||
import { findSLA } from './lib/sla';
|
||||
import { API } from '../../../../../server/api';
|
||||
import { getPaginationItems } from '../../../../../server/api/lib/getPaginationItems';
|
||||
import { LivechatEnterprise } from '../../../lib/omnichannel/LivechatEnterprise';
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/sla',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: {
|
||||
GET: { permissions: ['manage-livechat-sla', 'view-l-room'], operation: 'hasAny' },
|
||||
POST: { permissions: ['manage-livechat-sla'], operation: 'hasAny' },
|
||||
},
|
||||
validateParams: {
|
||||
GET: isLivechatPrioritiesProps,
|
||||
POST: isCreateOrUpdateLivechatSlaProps,
|
||||
},
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
const { text } = this.queryParams;
|
||||
|
||||
return API.v1.success(
|
||||
await findSLA({
|
||||
text,
|
||||
pagination: {
|
||||
offset,
|
||||
count,
|
||||
sort,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
async post() {
|
||||
const { name, description, dueTimeInMinutes } = this.bodyParams;
|
||||
|
||||
const newSla = await LivechatEnterprise.saveSLA(
|
||||
null,
|
||||
{
|
||||
name,
|
||||
description,
|
||||
dueTimeInMinutes,
|
||||
},
|
||||
this.userId,
|
||||
);
|
||||
|
||||
return API.v1.success({ sla: newSla });
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/sla/:slaId',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: {
|
||||
GET: { permissions: ['manage-livechat-sla', 'view-l-room'], operation: 'hasAny' },
|
||||
DELETE: { permissions: ['manage-livechat-sla'], operation: 'hasAny' },
|
||||
PUT: { permissions: ['manage-livechat-sla'], operation: 'hasAny' },
|
||||
},
|
||||
validateParams: {
|
||||
PUT: isCreateOrUpdateLivechatSlaProps,
|
||||
},
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { slaId } = this.urlParams;
|
||||
|
||||
const sla = await OmnichannelServiceLevelAgreements.findOneById(slaId);
|
||||
|
||||
if (!sla) {
|
||||
return API.v1.notFound(`SLA with id ${slaId} not found`);
|
||||
}
|
||||
return API.v1.success(sla);
|
||||
},
|
||||
async delete() {
|
||||
const { slaId } = this.urlParams;
|
||||
|
||||
await LivechatEnterprise.removeSLA(this.userId, slaId);
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
async put() {
|
||||
const { name, description, dueTimeInMinutes } = this.bodyParams;
|
||||
const { slaId } = this.urlParams;
|
||||
|
||||
const updatedSla = await LivechatEnterprise.saveSLA(
|
||||
slaId,
|
||||
{
|
||||
name,
|
||||
description,
|
||||
dueTimeInMinutes,
|
||||
},
|
||||
this.userId,
|
||||
);
|
||||
|
||||
return API.v1.success({ sla: updatedSla });
|
||||
},
|
||||
},
|
||||
);
|
||||
123
apps/meteor/ee/server/api/v1/omnichannel/tags.ts
Normal file
123
apps/meteor/ee/server/api/v1/omnichannel/tags.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
isPOSTLivechatTagsSaveParams,
|
||||
POSTLivechatTagsSaveSuccessResponse,
|
||||
isPOSTLivechatTagsDeleteParams,
|
||||
POSTLivechatTagsDeleteSuccessResponse,
|
||||
validateBadRequestErrorResponse,
|
||||
validateForbiddenErrorResponse,
|
||||
validateUnauthorizedErrorResponse,
|
||||
} from '@rocket.chat/rest-typings';
|
||||
|
||||
import { findTags, findTagById } from './lib/tags';
|
||||
import { API } from '../../../../../server/api';
|
||||
import type { ExtractRoutesFromAPI } from '../../../../../server/api/ApiClass';
|
||||
import { getPaginationItems } from '../../../../../server/api/lib/getPaginationItems';
|
||||
import { LivechatEnterprise } from '../../../lib/omnichannel/LivechatEnterprise';
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/tags',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: { GET: { permissions: ['view-l-room', 'manage-livechat-tags'], operation: 'hasAny' } },
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
const { text, viewAll, department } = this.queryParams;
|
||||
|
||||
return API.v1.success(
|
||||
await findTags({
|
||||
userId: this.userId,
|
||||
text,
|
||||
department,
|
||||
viewAll: viewAll === 'true',
|
||||
pagination: {
|
||||
offset,
|
||||
count,
|
||||
sort: typeof sort === 'string' ? JSON.parse(sort || '{}') : sort,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/tags/:tagId',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: { GET: { permissions: ['view-l-room', 'manage-livechat-tags'], operation: 'hasAny' } },
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async get() {
|
||||
const { tagId } = this.urlParams;
|
||||
|
||||
const tag = await findTagById({
|
||||
userId: this.userId,
|
||||
tagId,
|
||||
});
|
||||
|
||||
if (!tag) {
|
||||
return API.v1.notFound('Tag not found');
|
||||
}
|
||||
|
||||
return API.v1.success(tag);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const livechatTagsEndpoints = API.v1
|
||||
.post(
|
||||
'livechat/tags.save',
|
||||
{
|
||||
response: {
|
||||
200: POSTLivechatTagsSaveSuccessResponse,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
authRequired: true,
|
||||
permissions: ['manage-livechat-tags'],
|
||||
license: ['livechat-enterprise'],
|
||||
body: isPOSTLivechatTagsSaveParams,
|
||||
},
|
||||
async function action() {
|
||||
const { _id, tagData, tagDepartments } = this.bodyParams;
|
||||
|
||||
const result = await LivechatEnterprise.saveTag(_id, tagData, tagDepartments);
|
||||
|
||||
return API.v1.success(result);
|
||||
},
|
||||
)
|
||||
.post(
|
||||
'livechat/tags.delete',
|
||||
{
|
||||
response: {
|
||||
200: POSTLivechatTagsDeleteSuccessResponse,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
authRequired: true,
|
||||
permissions: ['manage-livechat-tags'],
|
||||
license: ['livechat-enterprise'],
|
||||
body: isPOSTLivechatTagsDeleteParams,
|
||||
},
|
||||
async function action() {
|
||||
const { id } = this.bodyParams;
|
||||
|
||||
await LivechatEnterprise.removeTag(id);
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
);
|
||||
|
||||
type LivechatTagsEndpoints = ExtractRoutesFromAPI<typeof livechatTagsEndpoints>;
|
||||
|
||||
declare module '@rocket.chat/rest-typings' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
|
||||
interface Endpoints extends LivechatTagsEndpoints {}
|
||||
}
|
||||
37
apps/meteor/ee/server/api/v1/omnichannel/transcript.ts
Normal file
37
apps/meteor/ee/server/api/v1/omnichannel/transcript.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
|
||||
import { LivechatRooms } from '@rocket.chat/models';
|
||||
|
||||
import { API } from '../../../../../server/api';
|
||||
import { canAccessRoomAsync } from '../../../../../server/lib/authorization/canAccessRoom';
|
||||
import { requestPdfTranscript } from '../../../lib/omnichannel/requestPdfTranscript';
|
||||
|
||||
API.v1.addRoute(
|
||||
'omnichannel/:rid/request-transcript',
|
||||
{ authRequired: true, permissionsRequired: ['request-pdf-transcript'], license: ['livechat-enterprise'] },
|
||||
{
|
||||
async post() {
|
||||
const room = await LivechatRooms.findOneById<Pick<IOmnichannelRoom, '_id' | 'open' | 'v' | 't' | 'pdfTranscriptFileId'>>(
|
||||
this.urlParams.rid,
|
||||
{
|
||||
projection: { _id: 1, open: 1, v: 1, t: 1, pdfTranscriptFileId: 1 },
|
||||
},
|
||||
);
|
||||
if (!room) {
|
||||
throw new Error('error-invalid-room');
|
||||
}
|
||||
|
||||
if (!(await canAccessRoomAsync(room, { _id: this.userId }))) {
|
||||
throw new Error('error-not-allowed');
|
||||
}
|
||||
|
||||
// Flow is as follows:
|
||||
// 1. On Test Mode, call Transcript.workOnPdf directly
|
||||
// 2. On Normal Mode, call QueueWorker.queueWork to queue the work
|
||||
// 3. OmnichannelTranscript.workOnPdf will be called by the worker to generate the transcript
|
||||
// 4. We be happy :)
|
||||
await requestPdfTranscript(room, this.userId);
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
},
|
||||
);
|
||||
134
apps/meteor/ee/server/api/v1/omnichannel/triggers.ts
Normal file
134
apps/meteor/ee/server/api/v1/omnichannel/triggers.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { isExternalServiceTrigger } from '@rocket.chat/core-typings';
|
||||
import { LivechatTrigger } from '@rocket.chat/models';
|
||||
import { isLivechatTriggerWebhookCallParams } from '@rocket.chat/rest-typings';
|
||||
import { isLivechatTriggerWebhookTestParams } from '@rocket.chat/rest-typings/src/v1/omnichannel';
|
||||
|
||||
import { callTriggerExternalService } from './lib/triggers';
|
||||
import { API } from '../../../../../server/api';
|
||||
import { settings } from '../../../../../server/settings';
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/triggers/external-service/test',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-livechat-manager'],
|
||||
validateParams: isLivechatTriggerWebhookTestParams,
|
||||
rateLimiterOptions: { numRequestsAllowed: 15, intervalTimeInMS: 60000 },
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async post() {
|
||||
const { webhookUrl, timeout, fallbackMessage, extraData: clientParams } = this.bodyParams;
|
||||
|
||||
const token = settings.get<string>('Livechat_secret_token');
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Livechat secret token is not configured');
|
||||
}
|
||||
|
||||
const body = {
|
||||
metadata: clientParams,
|
||||
visitorToken: '1234567890',
|
||||
};
|
||||
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-RocketChat-Livechat-Token': token,
|
||||
};
|
||||
|
||||
const response = await callTriggerExternalService({
|
||||
url: webhookUrl,
|
||||
timeout,
|
||||
fallbackMessage,
|
||||
body,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
return API.v1.failure({
|
||||
triggerId: 'test-trigger',
|
||||
...response,
|
||||
});
|
||||
}
|
||||
|
||||
return API.v1.success({
|
||||
triggerId: 'test-trigger',
|
||||
...response,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/triggers/:_id/external-service/call',
|
||||
{
|
||||
authRequired: false,
|
||||
rateLimiterOptions: {
|
||||
numRequestsAllowed: 10,
|
||||
intervalTimeInMS: 60000,
|
||||
},
|
||||
validateParams: isLivechatTriggerWebhookCallParams,
|
||||
license: ['livechat-enterprise'],
|
||||
},
|
||||
{
|
||||
async post() {
|
||||
const { _id: triggerId } = this.urlParams;
|
||||
const { token: visitorToken, extraData } = this.bodyParams;
|
||||
|
||||
const trigger = await LivechatTrigger.findOneById(triggerId);
|
||||
|
||||
if (!trigger) {
|
||||
throw new Error('Invalid trigger');
|
||||
}
|
||||
|
||||
if (!trigger?.actions.length || !isExternalServiceTrigger(trigger)) {
|
||||
throw new Error('Trigger is not configured to use an external service');
|
||||
}
|
||||
|
||||
const { params: { serviceTimeout = 5000, serviceUrl, serviceFallbackMessage = 'trigger-default-fallback-message' } = {} } =
|
||||
trigger.actions[0];
|
||||
|
||||
if (!serviceUrl) {
|
||||
throw new Error('Invalid service URL');
|
||||
}
|
||||
|
||||
const token = settings.get<string>('Livechat_secret_token');
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Livechat secret token is not configured');
|
||||
}
|
||||
|
||||
const body = {
|
||||
metadata: extraData,
|
||||
visitorToken,
|
||||
};
|
||||
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-RocketChat-Livechat-Token': token,
|
||||
};
|
||||
|
||||
const response = await callTriggerExternalService({
|
||||
url: serviceUrl,
|
||||
timeout: serviceTimeout,
|
||||
fallbackMessage: serviceFallbackMessage,
|
||||
body,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
return API.v1.failure({
|
||||
triggerId,
|
||||
...response,
|
||||
});
|
||||
}
|
||||
|
||||
return API.v1.success({
|
||||
triggerId,
|
||||
...response,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
153
apps/meteor/ee/server/api/v1/omnichannel/units.ts
Normal file
153
apps/meteor/ee/server/api/v1/omnichannel/units.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type { ILivechatUnitMonitor, IOmnichannelBusinessUnit, OmnichannelBusinessUnitPayload } from '@rocket.chat/core-typings';
|
||||
import type { PaginatedResult, PaginatedRequest } from '@rocket.chat/rest-typings';
|
||||
|
||||
import { findUnits, findUnitById, findUnitMonitors, findUnitsOfUser } from './lib/units';
|
||||
import { API } from '../../../../../server/api';
|
||||
import { getPaginationItems } from '../../../../../server/api/lib/getPaginationItems';
|
||||
import { hasPermissionAsync } from '../../../../../server/lib/authorization/hasPermission';
|
||||
import { findAllDepartmentsAvailable, findAllDepartmentsByUnit } from '../../../lib/omnichannel/Department';
|
||||
import { LivechatEnterprise } from '../../../lib/omnichannel/LivechatEnterprise';
|
||||
|
||||
declare module '@rocket.chat/rest-typings' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface Endpoints {
|
||||
'/v1/livechat/units/:unitId/monitors': {
|
||||
GET: (params: { unitId: string }) => { monitors: ILivechatUnitMonitor[] };
|
||||
};
|
||||
'/v1/livechat/units': {
|
||||
GET: (params: PaginatedRequest<{ text?: string }>) => PaginatedResult & { units: IOmnichannelBusinessUnit[] };
|
||||
POST: (params: OmnichannelBusinessUnitPayload) => Omit<IOmnichannelBusinessUnit, '_updatedAt'>;
|
||||
};
|
||||
'/v1/livechat/units/:id': {
|
||||
GET: () => IOmnichannelBusinessUnit;
|
||||
POST: (params: OmnichannelBusinessUnitPayload) => Omit<IOmnichannelBusinessUnit, '_updatedAt'>;
|
||||
DELETE: () => number;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/units/:unitId/monitors',
|
||||
{ authRequired: true, permissionsRequired: ['manage-livechat-monitors'], license: ['livechat-enterprise'] },
|
||||
{
|
||||
async get() {
|
||||
const { unitId } = this.urlParams;
|
||||
|
||||
if (!unitId) {
|
||||
return API.v1.failure('The "unitId" parameter is required');
|
||||
}
|
||||
return API.v1.success({
|
||||
monitors: await findUnitMonitors({
|
||||
unitId,
|
||||
}),
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/units',
|
||||
{ authRequired: true, permissionsRequired: { POST: ['manage-livechat-units'], GET: [] }, license: ['livechat-enterprise'] },
|
||||
{
|
||||
async get() {
|
||||
const params = this.queryParams;
|
||||
const { offset, count } = await getPaginationItems(params);
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
const { text } = this.queryParams;
|
||||
|
||||
if (!(await hasPermissionAsync(this.user, 'manage-livechat-units'))) {
|
||||
return API.v1.success(await findUnitsOfUser({ text, userId: this.userId, pagination: { offset, count, sort } }));
|
||||
}
|
||||
|
||||
return API.v1.success(
|
||||
await findUnits({
|
||||
text,
|
||||
pagination: {
|
||||
offset,
|
||||
count,
|
||||
sort,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
async post() {
|
||||
const { unitData, unitMonitors, unitDepartments } = this.bodyParams;
|
||||
return API.v1.success(await LivechatEnterprise.saveUnit(null, unitData, unitMonitors, unitDepartments, this.userId));
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/units/:id',
|
||||
{ authRequired: true, permissionsRequired: ['manage-livechat-units'], license: ['livechat-enterprise'] },
|
||||
{
|
||||
async get() {
|
||||
const { id } = this.urlParams;
|
||||
const unit = await findUnitById({
|
||||
unitId: id,
|
||||
userId: this.userId,
|
||||
});
|
||||
|
||||
return API.v1.success(unit);
|
||||
},
|
||||
async post() {
|
||||
const { unitData, unitMonitors, unitDepartments } = this.bodyParams;
|
||||
const { id } = this.urlParams;
|
||||
|
||||
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, this.userId)).deletedCount);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/units/:unitId/departments',
|
||||
{ authRequired: true, permissionsRequired: ['manage-livechat-units'], license: ['livechat-enterprise'] },
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { unitId } = this.urlParams;
|
||||
|
||||
const { departments, total } = await findAllDepartmentsByUnit(unitId, offset, count);
|
||||
|
||||
return API.v1.success({
|
||||
departments,
|
||||
count: departments.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.addRoute(
|
||||
'livechat/units/:unitId/departments/available',
|
||||
{ authRequired: true, permissionsRequired: ['manage-livechat-units'], license: ['livechat-enterprise'] },
|
||||
{
|
||||
async get() {
|
||||
const { offset, count } = await getPaginationItems(this.queryParams);
|
||||
const { unitId } = this.urlParams;
|
||||
const { text, onlyMyDepartments } = this.queryParams;
|
||||
|
||||
const { departments, total } = await findAllDepartmentsAvailable(
|
||||
this.userId,
|
||||
unitId,
|
||||
offset,
|
||||
count,
|
||||
text,
|
||||
onlyMyDepartments === 'true',
|
||||
);
|
||||
|
||||
return API.v1.success({
|
||||
departments,
|
||||
count: departments.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user