Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const getInitialData = (businessHourData: Serialized<ILivechatBusinessHour> | un
})),
departmentsToApplyBusinessHour: '',
active: businessHourData?.active ?? true,
departments: businessHourData?.departments?.map(({ _id, name }) => ({ value: _id, label: name })) || [],
departments: businessHourData?.departments?.map(({ _id, name }) => ({ value: _id, label: name ?? '' })) || [],
});

export type EditBusinessHoursProps = {
Expand Down
84 changes: 65 additions & 19 deletions apps/meteor/ee/server/api/v1/omnichannel/business-hours.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import type { PaginatedRequest } from '@rocket.chat/rest-typings';
import {
ajv,
ajvQuery,
validateBadRequestErrorResponse,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
} from '@rocket.chat/rest-typings';

import { API } from '../../../../../server/api';
import { getPaginationItems } from '../../../../../server/api/lib/getPaginationItems';
Expand All @@ -9,7 +16,7 @@ declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface Endpoints {
'/v1/livechat/business-hours': {
GET: (params: PaginatedRequest) => {
GET: (params: PaginatedRequest<{ name?: string }>) => {
businessHours: ILivechatBusinessHour[];
count: number;
offset: number;
Expand All @@ -19,26 +26,65 @@ declare module '@rocket.chat/rest-typings' {
}
}

API.v1.addRoute(
const businessHoursQueryValidator = ajvQuery.compile<PaginatedRequest<{ name?: string }>>({
type: 'object',
properties: {
count: { type: 'number' },
offset: { type: 'number' },
sort: { type: 'string' },
query: { type: 'string' },
name: { type: 'string' },
},
additionalProperties: false,
});

const businessHoursResponseSchema = ajv.compile<{
businessHours: ILivechatBusinessHour[];
count: number;
offset: number;
total: number;
}>({
type: 'object',
properties: {
businessHours: { type: 'array', items: { $ref: '#/components/schemas/ILivechatBusinessHour' } },
count: { type: 'number' },
offset: { type: 'number' },
total: { type: 'number' },
success: { type: 'boolean', enum: [true] },
},
required: ['businessHours', 'count', 'offset', 'total', 'success'],
additionalProperties: false,
});
Comment on lines +41 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the generated ILivechatDepartment/ILivechatBusinessHour schemas for strictness
rg -n "ILivechatDepartment" packages/core-typings/src/Ajv.ts -A 15
rg -n "ILivechatBusinessHour" packages/core-typings/src/Ajv.ts -A 15

Repository: RocketChat/Rocket.Chat

Length of output: 2501


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== business-hours route =="
sed -n '1,220p' apps/meteor/ee/server/api/v1/omnichannel/business-hours.ts

echo
echo "== generated schema definitions =="
rg -n "ILivechatDepartment|ILivechatBusinessHour" packages/core-typings/src/Ajv.ts -A 40 -B 10

echo
echo "== business hour data source =="
sed -n '1,240p' apps/meteor/ee/app/livechat-enterprise/server/business-hour/lib/business-hour.ts

Repository: RocketChat/Rocket.Chat

Length of output: 6592


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ILivechatBusinessHour type =="
rg -n "export interface ILivechatBusinessHour|type ILivechatBusinessHour" -A 80 -B 10 packages apps --glob '*LivechatBusinessHour*' --glob '*livechat*' --glob '*.ts'

echo
echo "== ILivechatDepartment type =="
rg -n "export interface ILivechatDepartment|type ILivechatDepartment" -A 120 -B 10 packages apps --glob '*LivechatDepartment*' --glob '*.ts'

echo
echo "== business-hour schema generation details =="
rg -n "ILivechatBusinessHour|ILivechatDepartment" packages/core-typings/src -A 120 -B 20

Repository: RocketChat/Rocket.Chat

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ILivechatBusinessHour type =="
rg -n "export (interface|type) ILivechatBusinessHour" -A 120 -B 10 packages apps --glob '*.ts'

echo
echo "== ILivechatDepartment type =="
rg -n "export (interface|type) ILivechatDepartment" -A 160 -B 10 packages apps --glob '*.ts'

echo
echo "== core-typings source for livechat schemas =="
rg -n "ILivechatBusinessHour|ILivechatDepartment" packages/core-typings/src -A 160 -B 20

Repository: RocketChat/Rocket.Chat

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== IRocketChatRecord =="
sed -n '1,120p' packages/core-typings/src/IRocketChatRecord.ts

echo
echo "== ILivechatDepartment component schema in Ajv.ts =="
python3 - <<'PY'
from pathlib import Path
text = Path('packages/core-typings/src/Ajv.ts').read_text()
for needle in ['"ILivechatDepartment"', '"ILivechatBusinessHour"', 'ILivechatDepartmentRecord']:
    idx = text.find(needle)
    print(f'-- {needle}:', idx)
    if idx != -1:
        start = max(0, idx - 1200)
        end = min(len(text), idx + 5000)
        print(text[start:end])
        print('\n' + '='*80 + '\n')
PY

Repository: RocketChat/Rocket.Chat

Length of output: 752


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== IRocketChatRecord =="
sed -n '1,120p' packages/core-typings/src/IRocketChatRecord.ts

echo
echo "== specific schema snippets from Ajv.ts =="
python3 - <<'PY'
from pathlib import Path
text = Path('packages/core-typings/src/Ajv.ts').read_text()
for needle in ['ILivechatDepartment', 'ILivechatBusinessHour', 'ILivechatDepartmentRecord']:
    idx = text.find(needle)
    print(f'-- {needle}: {idx}')
    if idx != -1:
        start = max(0, idx - 800)
        end = min(len(text), idx + 3500)
        print(text[start:end])
        print('\n' + '='*80 + '\n')
PY

Repository: RocketChat/Rocket.Chat

Length of output: 5860


Response schema rejects business-hours that include departments. findBusinessHours assigns departments from a { _id: 1 } projection, but the 200 schema still uses full ILivechatBusinessHour/ILivechatDepartment shapes. Any business hour with departments will fail response validation; either fetch full department docs or narrow the schema to the projected shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/ee/server/api/v1/omnichannel/business-hours.ts` around lines 28 -
44, Update businessHoursResponseSchema and its ILivechatBusinessHour response
type to match the department projection returned by findBusinessHours, allowing
the projected department shape instead of requiring the full ILivechatDepartment
fields. Preserve validation for the remaining business-hour fields and the
required response properties.


API.v1.get(
'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,
),
);
authRequired: true,
permissionsRequired: ['view-livechat-business-hours'],
license: ['livechat-enterprise'],
query: businessHoursQueryValidator,
response: {
200: businessHoursResponseSchema,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
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,
)),
});
},
Comment on lines +59 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if a pagination/name query schema already exists for reuse
rg -n "isGET.*BusinessHours.*Params|PaginatedRequest" packages/rest-typings/src/v1/omnichannel.ts | head -30

Repository: RocketChat/Rocket.Chat

Length of output: 2498


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== route file =="
sed -n '1,220p' apps/meteor/ee/server/api/v1/omnichannel/business-hours.ts

echo
echo "== sibling GET route =="
rg -n "livechat/business-hour|query:" apps/meteor/ee/server/api/v1/omnichannel -n

echo
echo "== possible query schema definitions =="
rg -n "isGET.*BusinessHour|BusinessHours.*Params|PaginatedRequest<.*name" apps packages -g '*.ts' | head -80

Repository: RocketChat/Rocket.Chat

Length of output: 5971


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' apps/meteor/ee/server/api/v1/omnichannel/business-hours.ts

Repository: RocketChat/Rocket.Chat

Length of output: 2230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' apps/meteor/server/api/v1/omnichannel/businessHours.ts
echo
sed -n '3230,3270p' packages/rest-typings/src/v1/omnichannel.ts

Repository: RocketChat/Rocket.Chat

Length of output: 3883


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '3230,3265p' packages/rest-typings/src/v1/omnichannel.ts

Repository: RocketChat/Rocket.Chat

Length of output: 855


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== getPaginationItems =="
sed -n '1,220p' server/api/lib/getPaginationItems.ts

echo
echo "== parseJsonQuery usage in API class =="
rg -n "parseJsonQuery\\(" server apps/meteor -g '*.ts' -g '*.js' | head -40

Repository: RocketChat/Rocket.Chat

Length of output: 265


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate getPaginationItems =="
fd -a "getPaginationItems.ts" apps packages server

echo
echo "== locate parseJsonQuery implementation =="
fd -a "ApiClass.ts" apps packages server | head -20

echo
echo "== locate API class files =="
fd -a "api.ts" apps packages server | head -40

Repository: RocketChat/Rocket.Chat

Length of output: 2009


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== getPaginationItems =="
sed -n '1,220p' apps/meteor/server/api/lib/getPaginationItems.ts

echo
echo "== parseJsonQuery in ApiClass =="
rg -n "parseJsonQuery|queryParams" apps/meteor/server/api/ApiClass.ts
sed -n '1,260p' apps/meteor/server/api/ApiClass.ts

Repository: RocketChat/Rocket.Chat

Length of output: 10173


Add the query schema here. offset, count, sort, and name are read directly from request input, but this route never passes a validator to API.v1.get, so they skip AJV validation unlike livechat/business-hour.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/ee/server/api/v1/omnichannel/business-hours.ts` around lines 46 -
75, Update the `API.v1.get` definition for `livechat/business-hours` to include
a query schema validating the request inputs consumed by `getPaginationItems`,
`parseJsonQuery`, and `this.queryParams`: `offset`, `count`, `sort`, and `name`.
Reuse the established schema used by the `livechat/business-hour` route so
validation behavior remains consistent.

);
54 changes: 40 additions & 14 deletions apps/meteor/ee/server/api/v1/omnichannel/inquiries.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,53 @@
import {
ajv,
validateBadRequestErrorResponse,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
} from '@rocket.chat/rest-typings';

import { setSLAToInquiry } from './lib/inquiries';
import { API } from '../../../../../server/api';

API.v1.addRoute(
const isPUTLivechatInquirySetSlaParams = ajv.compile<{ roomId: string; sla: string }>({
type: 'object',
properties: {
roomId: { type: 'string' },
sla: { type: 'string' },
},
required: ['roomId', 'sla'],
additionalProperties: false,
});

const inquirySetSlaResponseSchema = ajv.compile<void>({
type: 'object',
properties: { success: { type: 'boolean', enum: [true] } },
required: ['success'],
additionalProperties: false,
});

API.v1.put(
'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();
body: isPUTLivechatInquirySetSlaParams,
response: {
200: inquirySetSlaResponseSchema,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const { roomId, sla } = this.bodyParams;
await setSLAToInquiry({
userId: this.userId,
roomId,
sla,
});
return API.v1.success();
},
);
75 changes: 50 additions & 25 deletions apps/meteor/ee/server/api/v1/omnichannel/transcript.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,62 @@
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';
import {
ajv,
validateBadRequestErrorResponse,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
} from '@rocket.chat/rest-typings';

import { API } from '../../../../../server/api';
import type { ExtractRoutesFromAPI } from '../../../../../server/api/ApiClass';
import { canAccessRoomAsync } from '../../../../../server/lib/authorization/canAccessRoom';
import { requestPdfTranscript } from '../../../lib/omnichannel/requestPdfTranscript';

API.v1.addRoute(
const requestTranscriptResponseSchema = ajv.compile<void>({
type: 'object',
properties: { success: { type: 'boolean', enum: [true] } },
required: ['success'],
additionalProperties: false,
});

const requestTranscriptEndpoints = API.v1.post(
'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();
authRequired: true,
permissionsRequired: ['request-pdf-transcript'],
license: ['livechat-enterprise'],
body: ajv.compile<undefined>({ type: 'object', additionalProperties: false }),
response: {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
200: requestTranscriptResponseSchema,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
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) {
return API.v1.failure('error-invalid-room');
}

if (!(await canAccessRoomAsync(room, { _id: this.userId }))) {
return API.v1.failure('error-not-allowed');
}

await requestPdfTranscript(room, this.userId);

return API.v1.success();
},
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

type RequestTranscriptEndpoints = ExtractRoutesFromAPI<typeof requestTranscriptEndpoints>;

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends RequestTranscriptEndpoints {}
}
57 changes: 47 additions & 10 deletions apps/meteor/server/api/v1/omnichannel/agentDepartments.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,56 @@
import { isGETLivechatAgentsAgentIdDepartmentsParams } from '@rocket.chat/rest-typings';
import type { ILivechatDepartmentAgents } from '@rocket.chat/core-typings';
import {
ajv,
isGETLivechatAgentsAgentIdDepartmentsParams,
validateBadRequestErrorResponse,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
} from '@rocket.chat/rest-typings';

import { API } from '../..';
import { findAgentDepartments } from './lib/agents';

API.v1.addRoute(
const agentDepartmentsResponseSchema = ajv.compile<{ departments: (ILivechatDepartmentAgents & { departmentName: string })[] }>({
type: 'object',
properties: {
departments: {
type: 'array',
items: {
allOf: [
{ $ref: '#/components/schemas/ILivechatDepartmentAgents' },
{
type: 'object',
properties: { departmentName: { type: 'string' } },
required: ['departmentName'],
},
],
},
},
success: { type: 'boolean', enum: [true] },
},
required: ['departments', 'success'],
additionalProperties: false,
});

API.v1.get(
'livechat/agents/:agentId/departments',
{ authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatAgentsAgentIdDepartmentsParams },
{
async get() {
const departments = await findAgentDepartments({
enabledDepartmentsOnly: this.queryParams.enabledDepartmentsOnly && this.queryParams.enabledDepartmentsOnly === 'true',
agentId: this.urlParams.agentId,
});

return API.v1.success(departments);
authRequired: true,
permissionsRequired: ['view-l-room'],
query: isGETLivechatAgentsAgentIdDepartmentsParams,
response: {
200: agentDepartmentsResponseSchema,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const departments = await findAgentDepartments({
enabledDepartmentsOnly: this.queryParams.enabledDepartmentsOnly && this.queryParams.enabledDepartmentsOnly === 'true',
agentId: this.urlParams.agentId,
});

return API.v1.success(departments);
},
);
Loading
Loading