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 2s
542 lines
16 KiB
TypeScript
542 lines
16 KiB
TypeScript
import { Omnichannel } from '@rocket.chat/core-services';
|
|
import type {
|
|
ILivechatAgent,
|
|
IOmnichannelInquiryExtraData,
|
|
IOmnichannelRoom,
|
|
IUser,
|
|
SelectedAgent,
|
|
TransferByData,
|
|
TransferData,
|
|
} from '@rocket.chat/core-typings';
|
|
import { isOmnichannelRoom, OmnichannelSourceType } from '@rocket.chat/core-typings';
|
|
import { LivechatVisitors, Users, LivechatRooms } from '@rocket.chat/models';
|
|
import {
|
|
isLiveChatRoomForwardProps,
|
|
isPOSTLivechatRoomCloseParams,
|
|
isPOSTLivechatRoomSurveyParams,
|
|
isLiveChatRoomJoinProps,
|
|
isLiveChatRoomSaveInfoProps,
|
|
isPOSTLivechatRoomCloseByUserParams,
|
|
isPOSTLivechatRoomsCloseAll,
|
|
isPOSTLivechatRoomsCloseAllSuccessResponse,
|
|
POSTLivechatRemoveRoomSuccess,
|
|
isPOSTLivechatRemoveRoomParams,
|
|
validateBadRequestErrorResponse,
|
|
validateUnauthorizedErrorResponse,
|
|
validateForbiddenErrorResponse,
|
|
ajv,
|
|
} from '@rocket.chat/rest-typings';
|
|
import { isPOSTLivechatVisitorDepartmentTransferParams } from '@rocket.chat/rest-typings/src/v1/omnichannel';
|
|
import { check, Match } from 'meteor/check';
|
|
import { Meteor } from 'meteor/meteor';
|
|
|
|
import { API } from '../..';
|
|
import { findGuest, findRoom, settings, findAgent, onCheckRoomParams } from './lib/livechat';
|
|
import { canAccessRoomAsync } from '../../../lib/authorization';
|
|
import { hasPermissionAsync } from '../../../lib/authorization/hasPermission';
|
|
import { callbacks } from '../../../lib/callbacks';
|
|
import { i18n } from '../../../lib/i18n';
|
|
import { normalizeTransferredByData } from '../../../lib/omnichannel/Helper';
|
|
import { closeLivechatRoom } from '../../../lib/omnichannel/closeLivechatRoom';
|
|
import { closeRoom } from '../../../lib/omnichannel/closeRoom';
|
|
import { saveGuest } from '../../../lib/omnichannel/guests';
|
|
import type { CloseRoomParams } from '../../../lib/omnichannel/localTypes';
|
|
import { livechatLogger } from '../../../lib/omnichannel/logger';
|
|
import { createRoom, removeOmnichannelRoom, saveRoomInfo } from '../../../lib/omnichannel/rooms';
|
|
import { transfer } from '../../../lib/omnichannel/transfer';
|
|
import { addUserToRoom } from '../../../lib/rooms/addUserToRoom';
|
|
import { settings as rcSettings } from '../../../settings';
|
|
import type { ExtractRoutesFromAPI } from '../../ApiClass';
|
|
import { isWidget } from '../../lib/isWidget';
|
|
|
|
const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj);
|
|
|
|
API.v1.addRoute(
|
|
'livechat/room',
|
|
{
|
|
rateLimiterOptions: {
|
|
numRequestsAllowed: 5,
|
|
intervalTimeInMS: 60000,
|
|
},
|
|
},
|
|
{
|
|
async get() {
|
|
// I'll temporary use check for validation, as validateParams doesnt support what's being done here
|
|
const extraCheckParams = onCheckRoomParams({
|
|
token: String,
|
|
rid: Match.Maybe(String),
|
|
agentId: Match.Maybe(String),
|
|
});
|
|
|
|
check(this.queryParams, extraCheckParams);
|
|
|
|
const { token, rid, agentId, ...extraParams } = this.queryParams;
|
|
|
|
const guest = token && (await findGuest(token));
|
|
if (!guest) {
|
|
throw new Error('invalid-token');
|
|
}
|
|
|
|
if (!rid) {
|
|
const room = await LivechatRooms.findOneOpenByVisitorToken(token, {});
|
|
if (room) {
|
|
return API.v1.success({ room, newRoom: false });
|
|
}
|
|
|
|
let agent: SelectedAgent | undefined;
|
|
const agentObj = agentId && (await findAgent(agentId));
|
|
if (agentObj) {
|
|
if (isAgentWithInfo(agentObj)) {
|
|
const { username = undefined } = agentObj;
|
|
agent = { agentId, username };
|
|
} else {
|
|
agent = { agentId };
|
|
}
|
|
}
|
|
|
|
const roomInfo = {
|
|
source: {
|
|
...(isWidget(this.request.headers)
|
|
? { type: OmnichannelSourceType.WIDGET, destination: this.request.headers.get('host')! }
|
|
: { type: OmnichannelSourceType.API }),
|
|
},
|
|
};
|
|
|
|
const newRoom = await createRoom({
|
|
visitor: guest,
|
|
roomInfo,
|
|
agent,
|
|
extraData: extraParams as IOmnichannelInquiryExtraData,
|
|
});
|
|
|
|
return API.v1.success({
|
|
room: newRoom,
|
|
newRoom: true,
|
|
});
|
|
}
|
|
|
|
const froom = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(rid, token, {});
|
|
if (!froom) {
|
|
throw new Error('invalid-room');
|
|
}
|
|
|
|
return API.v1.success({ room: froom, newRoom: false });
|
|
},
|
|
},
|
|
);
|
|
|
|
// Note: use this route if a visitor is closing a room
|
|
// If a RC user(like eg agent) is closing a room, use the `livechat/room.closeByUser` route
|
|
API.v1.addRoute(
|
|
'livechat/room.close',
|
|
{ validateParams: isPOSTLivechatRoomCloseParams },
|
|
{
|
|
async post() {
|
|
const { rid, token } = this.bodyParams;
|
|
|
|
if (!rcSettings.get('Omnichannel_allow_visitors_to_close_conversation')) {
|
|
throw new Error('error-not-allowed-to-close-conversation');
|
|
}
|
|
|
|
const visitor = await findGuest(token);
|
|
if (!visitor) {
|
|
throw new Error('invalid-token');
|
|
}
|
|
|
|
const room = await findRoom(token, rid);
|
|
if (!room) {
|
|
throw new Error('invalid-room');
|
|
}
|
|
|
|
if (!room.open) {
|
|
throw new Error('room-closed');
|
|
}
|
|
|
|
const language = rcSettings.get<string>('Language') || 'en';
|
|
const comment = i18n.t('Closed_by_visitor', { lng: language });
|
|
|
|
const options: CloseRoomParams['options'] = {};
|
|
if (room.servedBy) {
|
|
const servingAgent: Pick<IUser, '_id' | 'name' | 'username' | 'utcOffset' | 'settings' | 'language'> | null =
|
|
await Users.findOneById(room.servedBy._id, {
|
|
projection: {
|
|
name: 1,
|
|
username: 1,
|
|
utcOffset: 1,
|
|
settings: 1,
|
|
language: 1,
|
|
},
|
|
});
|
|
|
|
if (servingAgent?.settings?.preferences?.omnichannelTranscriptPDF) {
|
|
options.pdfTranscript = {
|
|
requestedBy: servingAgent._id,
|
|
};
|
|
}
|
|
|
|
// We'll send the transcript by email only if the setting is disabled (that means, we're not asking the user if he wants to receive the transcript by email)
|
|
// And the agent has the preference enabled to send the transcript by email and the visitor has an email address
|
|
// When Livechat_enable_transcript is enabled, the email will be sent via livechat/transcript route
|
|
if (
|
|
!rcSettings.get<boolean>('Livechat_enable_transcript') &&
|
|
servingAgent?.settings?.preferences?.omnichannelTranscriptEmail &&
|
|
visitor.visitorEmails?.length &&
|
|
visitor.visitorEmails?.[0]?.address
|
|
) {
|
|
const visitorEmail = visitor.visitorEmails?.[0]?.address;
|
|
|
|
const language = servingAgent.language || rcSettings.get<string>('Language') || 'en';
|
|
const t = i18n.getFixedT(language);
|
|
const subject = t('Transcript_of_your_livechat_conversation');
|
|
|
|
options.emailTranscript = {
|
|
sendToVisitor: true,
|
|
requestData: {
|
|
email: visitorEmail,
|
|
requestedAt: new Date(),
|
|
requestedBy: servingAgent,
|
|
subject,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
await closeRoom({ visitor, room, comment, options });
|
|
|
|
return API.v1.success({ rid, comment });
|
|
},
|
|
},
|
|
);
|
|
|
|
API.v1.addRoute(
|
|
'livechat/room.closeByUser',
|
|
{
|
|
validateParams: isPOSTLivechatRoomCloseByUserParams,
|
|
authRequired: true,
|
|
permissionsRequired: ['close-livechat-room'],
|
|
},
|
|
{
|
|
async post() {
|
|
const { rid, comment, tags, generateTranscriptPdf, transcriptEmail, forceClose } = this.bodyParams;
|
|
|
|
const allowForceClose = rcSettings.get<boolean>('Omnichannel_allow_force_close_conversations');
|
|
const isForceClosing = allowForceClose && forceClose;
|
|
|
|
if (isForceClosing) {
|
|
livechatLogger.warn({ msg: 'Force closing a conversation', user: this.userId, room: rid });
|
|
}
|
|
|
|
await closeLivechatRoom(this.user, rid, {
|
|
comment,
|
|
tags,
|
|
generateTranscriptPdf,
|
|
transcriptEmail,
|
|
forceClose: isForceClosing,
|
|
});
|
|
|
|
return API.v1.success();
|
|
},
|
|
},
|
|
);
|
|
|
|
API.v1.addRoute(
|
|
'livechat/room.survey',
|
|
{ validateParams: isPOSTLivechatRoomSurveyParams },
|
|
{
|
|
async post() {
|
|
const { rid, token, data } = this.bodyParams;
|
|
|
|
const visitor = await findGuest(token);
|
|
if (!visitor) {
|
|
throw new Error('invalid-token');
|
|
}
|
|
|
|
const room = await findRoom(token, rid);
|
|
if (!room) {
|
|
throw new Error('invalid-room');
|
|
}
|
|
|
|
const config = await settings();
|
|
if (!config.survey?.items || !config.survey.values) {
|
|
throw new Error('invalid-livechat-config');
|
|
}
|
|
|
|
const updateData: { [k: string]: string } = {};
|
|
for (const item of data) {
|
|
if ((config.survey.items.includes(item.name) && config.survey.values.includes(item.value)) || item.name === 'additionalFeedback') {
|
|
updateData[item.name] = item.value;
|
|
}
|
|
}
|
|
|
|
if (Object.keys(updateData).length === 0) {
|
|
throw new Error('invalid-data');
|
|
}
|
|
|
|
if (!(await LivechatRooms.updateSurveyFeedbackById(room._id, updateData))) {
|
|
return API.v1.failure();
|
|
}
|
|
|
|
return API.v1.success({ rid, data: updateData });
|
|
},
|
|
},
|
|
);
|
|
|
|
API.v1.addRoute(
|
|
'livechat/room.forward',
|
|
{ authRequired: true, permissionsRequired: ['view-l-room', 'transfer-livechat-guest'], validateParams: isLiveChatRoomForwardProps },
|
|
{
|
|
async post() {
|
|
const transferData = this.bodyParams as typeof this.bodyParams & {
|
|
transferredBy: TransferByData;
|
|
transferredTo?: { _id: string; username?: string; name?: string };
|
|
};
|
|
|
|
const room = await LivechatRooms.findOneById(this.bodyParams.roomId);
|
|
if (room?.t !== 'l') {
|
|
throw new Error('error-invalid-room');
|
|
}
|
|
|
|
if (!room.open) {
|
|
throw new Error('This_conversation_is_already_closed');
|
|
}
|
|
|
|
if (!(await Omnichannel.isWithinMACLimit(room))) {
|
|
throw new Error('error-mac-limit-reached');
|
|
}
|
|
|
|
const guest = await LivechatVisitors.findOneEnabledById(room.v?._id);
|
|
if (!guest) {
|
|
throw new Error('error-invalid-visitor');
|
|
}
|
|
|
|
transferData.transferredBy = normalizeTransferredByData(this.user, room);
|
|
if (transferData.userId) {
|
|
const userToTransfer = await Users.findOneById(transferData.userId);
|
|
if (userToTransfer) {
|
|
transferData.transferredTo = {
|
|
_id: userToTransfer._id,
|
|
username: userToTransfer.username,
|
|
name: userToTransfer.name,
|
|
};
|
|
}
|
|
}
|
|
|
|
const chatForwardedResult = await transfer(room, guest, transferData);
|
|
if (!chatForwardedResult) {
|
|
throw new Error('error-forwarding-chat');
|
|
}
|
|
|
|
return API.v1.success();
|
|
},
|
|
},
|
|
);
|
|
|
|
const livechatVisitorDepartmentTransfer = API.v1.post(
|
|
'livechat/visitor/department.transfer',
|
|
{
|
|
response: {
|
|
200: ajv.compile<void>({
|
|
type: 'object',
|
|
properties: {
|
|
success: {
|
|
type: 'boolean',
|
|
enum: [true],
|
|
},
|
|
},
|
|
required: ['success'],
|
|
additionalProperties: false,
|
|
}),
|
|
400: validateBadRequestErrorResponse,
|
|
},
|
|
body: isPOSTLivechatVisitorDepartmentTransferParams,
|
|
},
|
|
async function action() {
|
|
const { rid, token, department } = this.bodyParams;
|
|
|
|
const visitor = await findGuest(token);
|
|
if (!visitor) {
|
|
return API.v1.failure('invalid-token');
|
|
}
|
|
const room = await LivechatRooms.findOneById(rid);
|
|
|
|
if (room?.t !== 'l') {
|
|
return API.v1.failure('error-invalid-room');
|
|
}
|
|
|
|
if (!room.open) {
|
|
return API.v1.failure('This_conversation_is_already_closed');
|
|
}
|
|
|
|
// As this is a visitor endpoint, we should not show the mac limit error
|
|
if (!(await Omnichannel.isWithinMACLimit(room))) {
|
|
return API.v1.failure('error-transefing-chat');
|
|
}
|
|
|
|
const guest = await LivechatVisitors.findOneEnabledById(room.v?._id);
|
|
if (!guest) {
|
|
return API.v1.failure('error-invalid-visitor');
|
|
}
|
|
|
|
const transferredBy = normalizeTransferredByData(
|
|
{ _id: guest._id, username: guest.username, name: guest.name, userType: 'visitor' },
|
|
room,
|
|
);
|
|
|
|
const transferData: TransferData = { transferredBy, departmentId: department };
|
|
|
|
const chatForwardedResult = await transfer(room, guest, transferData);
|
|
if (!chatForwardedResult) {
|
|
return API.v1.failure('error-transfering-chat');
|
|
}
|
|
|
|
return API.v1.success();
|
|
},
|
|
);
|
|
|
|
type LivechatAnalyticsEndpoints = ExtractRoutesFromAPI<typeof livechatVisitorDepartmentTransfer>;
|
|
declare module '@rocket.chat/rest-typings' {
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
|
|
interface Endpoints extends LivechatAnalyticsEndpoints {}
|
|
}
|
|
|
|
API.v1.addRoute(
|
|
'livechat/room.join',
|
|
{ authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLiveChatRoomJoinProps },
|
|
{
|
|
async get() {
|
|
const { roomId } = this.queryParams;
|
|
|
|
const { user } = this;
|
|
|
|
if (!user) {
|
|
throw new Error('error-invalid-user');
|
|
}
|
|
|
|
const room = await LivechatRooms.findOneById(roomId);
|
|
|
|
if (!room) {
|
|
throw new Error('error-invalid-room');
|
|
}
|
|
|
|
if (!room.open) {
|
|
throw new Error('room-closed');
|
|
}
|
|
|
|
if (!(await Omnichannel.isWithinMACLimit(room))) {
|
|
throw new Error('error-mac-limit-reached');
|
|
}
|
|
|
|
if (!(await canAccessRoomAsync(room, user))) {
|
|
throw new Error('error-not-allowed');
|
|
}
|
|
|
|
await addUserToRoom(roomId, user);
|
|
|
|
return API.v1.success();
|
|
},
|
|
},
|
|
);
|
|
|
|
API.v1.addRoute(
|
|
'livechat/room.saveInfo',
|
|
{ authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLiveChatRoomSaveInfoProps },
|
|
{
|
|
async post() {
|
|
const { roomData, guestData } = this.bodyParams;
|
|
const room = await LivechatRooms.findOneById(roomData._id);
|
|
if (!room || !isOmnichannelRoom(room)) {
|
|
throw new Error('error-invalid-room');
|
|
}
|
|
|
|
if (
|
|
(!room.servedBy || room.servedBy._id !== this.userId) &&
|
|
!(await hasPermissionAsync(this.user, 'save-others-livechat-room-info'))
|
|
) {
|
|
return API.v1.forbidden();
|
|
}
|
|
|
|
if (room.sms) {
|
|
delete guestData.phone;
|
|
}
|
|
|
|
// We want this both operations to be concurrent, so we have to go with Promise.allSettled
|
|
const result = await Promise.allSettled([saveGuest(guestData, this.userId), saveRoomInfo(roomData)]);
|
|
|
|
const firstError = result.find((item) => item.status === 'rejected');
|
|
if (firstError) {
|
|
throw new Error(firstError.reason.error);
|
|
}
|
|
|
|
await callbacks.run('livechat.saveInfo', await LivechatRooms.findOneById(roomData._id), {
|
|
user: this.user,
|
|
oldRoom: room,
|
|
});
|
|
|
|
return API.v1.success();
|
|
},
|
|
},
|
|
);
|
|
|
|
const livechatRoomsEndpoints = API.v1
|
|
.post(
|
|
'livechat/rooms.delete',
|
|
{
|
|
response: {
|
|
200: POSTLivechatRemoveRoomSuccess,
|
|
400: validateBadRequestErrorResponse,
|
|
401: validateUnauthorizedErrorResponse,
|
|
403: validateForbiddenErrorResponse,
|
|
},
|
|
authRequired: true,
|
|
permissionsRequired: ['remove-closed-livechat-room'],
|
|
body: isPOSTLivechatRemoveRoomParams,
|
|
},
|
|
async function action() {
|
|
const { roomId } = this.bodyParams;
|
|
|
|
try {
|
|
await removeOmnichannelRoom(roomId);
|
|
return API.v1.success();
|
|
} catch (error: unknown) {
|
|
if (error instanceof Meteor.Error) {
|
|
return API.v1.failure(error.reason);
|
|
}
|
|
|
|
return API.v1.failure('error-removing-room');
|
|
}
|
|
},
|
|
)
|
|
.post(
|
|
'livechat/rooms.removeAllClosedRooms',
|
|
{
|
|
response: {
|
|
200: isPOSTLivechatRoomsCloseAllSuccessResponse,
|
|
},
|
|
authRequired: true,
|
|
permissionsRequired: ['remove-closed-livechat-rooms'],
|
|
body: isPOSTLivechatRoomsCloseAll,
|
|
},
|
|
async function action() {
|
|
livechatLogger.info({ msg: 'User is removing all closed rooms', userId: this.userId });
|
|
|
|
const params = this.bodyParams;
|
|
|
|
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: this.userId });
|
|
const promises: Promise<void>[] = [];
|
|
await LivechatRooms.findClosedRooms(params?.departmentIds, {}, extraQuery).forEach(({ _id }: IOmnichannelRoom) => {
|
|
promises.push(removeOmnichannelRoom(_id));
|
|
});
|
|
await Promise.all(promises);
|
|
|
|
livechatLogger.info({ msg: 'User removed closed rooms', userId: this.userId, removedRooms: promises.length });
|
|
return API.v1.success({ removedRooms: promises.length });
|
|
},
|
|
);
|
|
|
|
type LivechatRoomsEndpoints = ExtractRoutesFromAPI<typeof livechatRoomsEndpoints>;
|
|
|
|
declare module '@rocket.chat/rest-typings' {
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
|
|
interface Endpoints extends LivechatRoomsEndpoints {}
|
|
}
|