diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 821d1fdd60d53..f2521d2f8cf5d 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -10,7 +10,9 @@ import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@roc import { callbacks } from '../../../../lib/callbacks'; import { deasyncPromise } from '../../../../server/deasync/deasync'; -import { type ILivechatMessage, Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; +import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; +import { getRoomMessages } from '../../../livechat/server/lib/getRoomMessages'; +import type { ILivechatMessage } from '../../../livechat/server/lib/localTypes'; import { settings } from '../../../settings/server'; declare module '@rocket.chat/apps/dist/converters/IAppMessagesConverter' { @@ -352,7 +354,7 @@ export class AppLivechatBridge extends LivechatBridge { throw new Error('Could not get the message converter to process livechat room messages'); } - const livechatMessages = await LivechatTyped.getRoomMessages({ rid: roomId }); + const livechatMessages = await getRoomMessages({ rid: roomId }); return Promise.all(await livechatMessages.map((message) => messageConverter.convertMessage(message, livechatMessages)).toArray()); } diff --git a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts index 263b137ae00c6..2ba29ecd32d20 100644 --- a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts +++ b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts @@ -3,8 +3,8 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms, Subscriptions } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import type { CloseRoomParams } from '../../../livechat/server/lib/LivechatTyped'; import { Livechat } from '../../../livechat/server/lib/LivechatTyped'; +import type { CloseRoomParams } from '../../../livechat/server/lib/localTypes'; import { notifyOnSubscriptionChanged } from '../lib/notifyListener'; export const closeLivechatRoom = async ( diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index e56feeac2fa3c..580356876dad4 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -15,7 +15,7 @@ import { findArchivedDepartments, } from '../../../server/api/lib/departments'; import { DepartmentHelper } from '../../../server/lib/Departments'; -import { Livechat as LivechatTs } from '../../../server/lib/LivechatTyped'; +import { saveDepartment, archiveDepartment, unarchiveDepartment, saveDepartmentAgents } from '../../../server/lib/departmentsLib'; import { isDepartmentCreationAvailable } from '../../../server/lib/isDepartmentCreationAvailable'; API.v1.addRoute( @@ -62,7 +62,7 @@ API.v1.addRoute( const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {}; const { departmentUnit } = this.bodyParams; - const department = await LivechatTs.saveDepartment( + const department = await saveDepartment( this.userId, null, this.bodyParams.department as ILivechatDepartment, @@ -131,7 +131,7 @@ API.v1.addRoute( } const agentParam = permissionToAddAgents && agents ? { upsert: agents } : {}; - await LivechatTs.saveDepartment(this.userId, _id, department, agentParam, departmentUnit || {}); + await saveDepartment(this.userId, _id, department, agentParam, departmentUnit || {}); return API.v1.success({ department: await LivechatDepartment.findOneById(_id), @@ -191,7 +191,7 @@ API.v1.addRoute( }, { async post() { - await LivechatTs.archiveDepartment(this.urlParams._id); + await archiveDepartment(this.urlParams._id); return API.v1.success(); }, @@ -206,7 +206,7 @@ API.v1.addRoute( }, { async post() { - await LivechatTs.unarchiveDepartment(this.urlParams._id); + await unarchiveDepartment(this.urlParams._id); return API.v1.success(); }, }, @@ -271,7 +271,7 @@ API.v1.addRoute( remove: Array, }), ); - await LivechatTs.saveDepartmentAgents(this.urlParams._id, this.bodyParams); + await saveDepartmentAgents(this.urlParams._id, this.bodyParams); return API.v1.success(); }, diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.ts b/apps/meteor/app/livechat/imports/server/rest/sms.ts index 15f08cdc1e83b..6d5b8227b74bc 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/sms.ts @@ -20,8 +20,8 @@ import { FileUpload } from '../../../../file-upload/server'; import { checkUrlForSsrf } from '../../../../lib/server/functions/checkUrlForSsrf'; import { settings } from '../../../../settings/server'; import { setCustomField } from '../../../server/api/lib/customFields'; -import type { ILivechatMessage } from '../../../server/lib/LivechatTyped'; import { Livechat as LivechatTyped } from '../../../server/lib/LivechatTyped'; +import type { ILivechatMessage } from '../../../server/lib/localTypes'; const logger = new Logger('SMS'); diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 7d52617e074ab..5d327e3d4ad76 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -23,8 +23,8 @@ import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom'; import { closeLivechatRoom } from '../../../../lib/server/functions/closeLivechatRoom'; import { settings as rcSettings } from '../../../../settings/server'; import { normalizeTransferredByData } from '../../lib/Helper'; -import type { CloseRoomParams } from '../../lib/LivechatTyped'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; +import type { CloseRoomParams } from '../../lib/localTypes'; import { findGuest, findRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat'; const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj); diff --git a/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts b/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts index f6a35f4dd7f9f..f0c445a78a788 100644 --- a/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts +++ b/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts @@ -3,7 +3,7 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; -import type { CloseRoomParams } from '../lib/LivechatTyped'; +import type { CloseRoomParams } from '../lib/localTypes'; import { sendTranscript } from '../lib/sendTranscript'; type LivechatCloseCallbackParams = { diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index a050e06c19433..76a192dc10782 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -47,6 +47,7 @@ import { settings } from '../../../settings/server'; import { Livechat as LivechatTyped } from './LivechatTyped'; import { queueInquiry, saveQueueInquiry } from './QueueManager'; import { RoutingManager } from './RoutingManager'; +import { getOnlineAgents } from './getOnlineAgents'; const logger = new Logger('LivechatHelper'); export const allowAgentSkipQueue = (agent: SelectedAgent) => { @@ -403,7 +404,7 @@ export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, age await saveQueueInquiry(inquiry); // Alert only the online agents of the queued request - const onlineAgents = await LivechatTyped.getOnlineAgents(department, agent); + const onlineAgents = await getOnlineAgents(department, agent); if (!onlineAgents) { logger.debug('Cannot notify agents of queued inquiry. No online agents found'); return; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index e521ac98fe711..18fcf74ca2780 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -7,7 +7,6 @@ import type { IOmnichannelRoom, IOmnichannelRoomClosingInfo, IUser, - MessageTypesValues, ILivechatVisitor, SelectedAgent, ILivechatAgent, @@ -15,11 +14,7 @@ import type { ILivechatDepartment, AtLeast, TransferData, - MessageAttachment, - IMessageInbox, IOmnichannelAgent, - ILivechatDepartmentAgents, - LivechatDepartmentDTO, ILivechatInquiryRecord, ILivechatContact, ILivechatContactChannel, @@ -43,7 +38,7 @@ import { import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import type { Filter, FindCursor, ClientSession, MongoError } from 'mongodb'; +import type { Filter, ClientSession, MongoError } from 'mongodb'; import UAParser from 'ua-parser-js'; import { callbacks } from '../../../../lib/callbacks'; @@ -65,7 +60,6 @@ import { notifyOnRoomChangedById, notifyOnLivechatInquiryChangedByToken, notifyOnUserChange, - notifyOnLivechatDepartmentAgentChangedByDepartmentId, notifyOnSubscriptionChangedByRoomId, notifyOnSubscriptionChanged, } from '../../../lib/server/lib/notifyListener'; @@ -77,8 +71,7 @@ import { createContact, createContactFromVisitor, isSingleContactEnabled } from import { parseAgentCustomFields, updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; -import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable'; -import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './localTypes'; +import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor, ILivechatMessage } from './localTypes'; import { parseTranscriptRequest } from './parseTranscriptRequest'; type RegisterGuestType = Partial> & { @@ -96,30 +89,6 @@ type OfflineMessageData = { host?: string; }; -type UploadedFile = { - _id: string; - name?: string; - type?: string; - size?: number; - description?: string; - identify?: { size: { width: number; height: number } }; - format?: string; -}; - -export interface ILivechatMessage { - token: string; - _id: string; - rid: string; - msg: string; - file?: UploadedFile; - files?: UploadedFile[]; - attachments?: MessageAttachment[]; - alias?: string; - groupable?: boolean; - blocks?: IMessage['blocks']; - email?: IMessageInbox['email']; -} - type AKeyOf = { [K in keyof T]?: T[K]; }; @@ -189,27 +158,6 @@ class LivechatClass { return agentsOnline; } - async getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise | undefined> { - if (agent?.agentId) { - return Users.findOnlineAgents(agent.agentId); - } - - if (department) { - const departmentAgents = await LivechatDepartmentAgents.getOnlineForDepartment(department); - if (!departmentAgents) { - return; - } - - const agentIds = await departmentAgents.map(({ agentId }) => agentId).toArray(); - if (!agentIds.length) { - return; - } - - return Users.findByIds([...new Set(agentIds)]); - } - return Users.findOnlineAgents(); - } - async closeRoom(params: CloseRoomParams, attempts = 2): Promise { let newRoom: IOmnichannelRoom; let chatCloser: ChatCloser; @@ -963,57 +911,6 @@ class LivechatClass { }); } - async getRoomMessages({ rid }: { rid: string }) { - const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); - if (room?.t !== 'l') { - throw new Meteor.Error('invalid-room'); - } - - const ignoredMessageTypes: MessageTypesValues[] = [ - 'livechat_navigation_history', - 'livechat_transcript_history', - 'command', - 'livechat-close', - 'livechat-started', - 'livechat_video_call', - ]; - - return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { - sort: { ts: 1 }, - }); - } - - async archiveDepartment(_id: string) { - const department = await LivechatDepartment.findOneById>(_id, { - projection: { _id: 1, businessHourId: 1 }, - }); - - if (!department) { - throw new Error('department-not-found'); - } - - await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]); - - void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); - - await callbacks.run('livechat.afterDepartmentArchived', department); - } - - async unarchiveDepartment(_id: string) { - const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found'); - } - - // TODO: these kind of actions should be on events instead of here - await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); - - void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); - - return true; - } - async updateMessage({ guest, message }: { guest: ILivechatVisitor; message: AtLeast }) { check(message, Match.ObjectIncluding({ _id: String })); @@ -1733,41 +1630,6 @@ class LivechatClass { return false; } - async saveDepartmentAgents( - _id: string, - departmentAgents: { - upsert?: Pick[]; - remove?: Pick[]; - }, - ) { - check(_id, String); - check(departmentAgents, { - upsert: Match.Maybe([ - Match.ObjectIncluding({ - agentId: String, - username: String, - count: Match.Maybe(Match.Integer), - order: Match.Maybe(Match.Integer), - }), - ]), - remove: Match.Maybe([ - Match.ObjectIncluding({ - agentId: String, - username: Match.Maybe(String), - count: Match.Maybe(Match.Integer), - order: Match.Maybe(Match.Integer), - }), - ]), - }); - - const department = await LivechatDepartment.findOneById>(_id, { projection: { enabled: 1 } }); - if (!department) { - throw new Meteor.Error('error-department-not-found', 'Department not found'); - } - - return updateDepartmentAgents(_id, departmentAgents, department.enabled); - } - async saveRoomInfo( roomData: { _id: string; @@ -1839,135 +1701,6 @@ class LivechatClass { return true; } - - /** - * @param {string|null} _id - The department id - * @param {Partial} departmentData - * @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }}} [departmentAgents] - The department agents - * @param {{_id?: string}} [departmentUnit] - The department's unit id - */ - async saveDepartment( - userId: string, - _id: string | null, - departmentData: LivechatDepartmentDTO, - departmentAgents?: { - upsert?: { agentId: string; count?: number; order?: number }[]; - remove?: { agentId: string; count?: number; order?: number }; - }, - departmentUnit?: { _id?: string }, - ) { - check(_id, Match.Maybe(String)); - if (departmentUnit?._id !== undefined && typeof departmentUnit._id !== 'string') { - throw new Meteor.Error('error-invalid-department-unit', 'Invalid department unit id provided', { - method: 'livechat:saveDepartment', - }); - } - - const department = _id - ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1, parentId: 1 } }) - : null; - - if (departmentUnit && !departmentUnit._id && department && department.parentId) { - const isLastDepartmentInUnit = (await LivechatDepartment.countDepartmentsInUnit(department.parentId)) === 1; - if (isLastDepartmentInUnit) { - throw new Meteor.Error('error-unit-cant-be-empty', "The last department in a unit can't be removed", { - method: 'livechat:saveDepartment', - }); - } - } - - if (!department && !(await isDepartmentCreationAvailable())) { - throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { - method: 'livechat:saveDepartment', - }); - } - - if (department?.archived && departmentData.enabled) { - throw new Meteor.Error('error-archived-department-cant-be-enabled', 'Archived departments cant be enabled', { - method: 'livechat:saveDepartment', - }); - } - - const defaultValidations: Record | BooleanConstructor | StringConstructor> = { - enabled: Boolean, - name: String, - description: Match.Optional(String), - showOnRegistration: Boolean, - email: String, - showOnOfflineForm: Boolean, - requestTagBeforeClosingChat: Match.Optional(Boolean), - chatClosingTags: Match.Optional([String]), - fallbackForwardDepartment: Match.Optional(String), - departmentsAllowedToForward: Match.Optional([String]), - allowReceiveForwardOffline: Match.Optional(Boolean), - }; - - // The Livechat Form department support addition/custom fields, so those fields need to be added before validating - Object.keys(departmentData).forEach((field) => { - if (!defaultValidations.hasOwnProperty(field)) { - defaultValidations[field] = Match.OneOf(String, Match.Integer, Boolean); - } - }); - - check(departmentData, defaultValidations); - check( - departmentAgents, - Match.Maybe({ - upsert: Match.Maybe(Array), - remove: Match.Maybe(Array), - }), - ); - - const { requestTagBeforeClosingChat, chatClosingTags, fallbackForwardDepartment } = departmentData; - if (requestTagBeforeClosingChat && (!chatClosingTags || chatClosingTags.length === 0)) { - throw new Meteor.Error( - 'error-validating-department-chat-closing-tags', - 'At least one closing tag is required when the department requires tag(s) on closing conversations.', - { method: 'livechat:saveDepartment' }, - ); - } - - if (_id && !department) { - throw new Meteor.Error('error-department-not-found', 'Department not found', { - method: 'livechat:saveDepartment', - }); - } - - if (fallbackForwardDepartment === _id) { - throw new Meteor.Error( - 'error-fallback-department-circular', - 'Cannot save department. Circular reference between fallback department and department', - ); - } - - if (fallbackForwardDepartment) { - const fallbackDep = await LivechatDepartment.findOneById(fallbackForwardDepartment, { - projection: { _id: 1, fallbackForwardDepartment: 1 }, - }); - if (!fallbackDep) { - throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', { - method: 'livechat:saveDepartment', - }); - } - } - - const departmentDB = await LivechatDepartment.createOrUpdateDepartment(_id, departmentData); - if (departmentDB && departmentAgents) { - await updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled); - } - - // Disable event - if (department?.enabled && !departmentDB?.enabled) { - await callbacks.run('livechat.afterDepartmentDisabled', departmentDB); - } - - if (departmentUnit) { - await callbacks.run('livechat.manageDepartmentUnit', { userId, departmentId: departmentDB._id, unitId: departmentUnit._id }); - } - - return departmentDB; - } } export const Livechat = new LivechatClass(); -export * from './localTypes'; diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index c6728d470870b..1644316e9920c 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -28,6 +28,7 @@ import { i18n } from '../../../utils/lib/i18n'; import { createLivechatRoom, createLivechatInquiry, allowAgentSkipQueue } from './Helper'; import { Livechat } from './LivechatTyped'; import { RoutingManager } from './RoutingManager'; +import { getOnlineAgents } from './getOnlineAgents'; import { getInquirySortMechanismSetting } from './settings'; const logger = new Logger('QueueManager'); @@ -333,7 +334,7 @@ export class QueueManager { const { department, rid, v } = inquiry; // Alert only the online agents of the queued request - const onlineAgents = await Livechat.getOnlineAgents(department, agent); + const onlineAgents = await getOnlineAgents(department, agent); if (!onlineAgents) { logger.debug('Cannot notify agents of queued inquiry. No online agents found'); diff --git a/apps/meteor/app/livechat/server/lib/departmentsLib.ts b/apps/meteor/app/livechat/server/lib/departmentsLib.ts new file mode 100644 index 0000000000000..79b8d40ed3874 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/departmentsLib.ts @@ -0,0 +1,201 @@ +import type { LivechatDepartmentDTO, ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; +import { LivechatDepartment, LivechatDepartmentAgents } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnLivechatDepartmentAgentChangedByDepartmentId } from '../../../lib/server/lib/notifyListener'; +import { updateDepartmentAgents } from './Helper'; +import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable'; +/** + * @param {string|null} _id - The department id + * @param {Partial} departmentData + * @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }}} [departmentAgents] - The department agents + * @param {{_id?: string}} [departmentUnit] - The department's unit id + */ +export async function saveDepartment( + userId: string, + _id: string | null, + departmentData: LivechatDepartmentDTO, + departmentAgents?: { + upsert?: { agentId: string; count?: number; order?: number }[]; + remove?: { agentId: string; count?: number; order?: number }; + }, + departmentUnit?: { _id?: string }, +) { + check(_id, Match.Maybe(String)); + if (departmentUnit?._id !== undefined && typeof departmentUnit._id !== 'string') { + throw new Meteor.Error('error-invalid-department-unit', 'Invalid department unit id provided', { + method: 'livechat:saveDepartment', + }); + } + + const department = _id + ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1, parentId: 1 } }) + : null; + + if (departmentUnit && !departmentUnit._id && department && department.parentId) { + const isLastDepartmentInUnit = (await LivechatDepartment.countDepartmentsInUnit(department.parentId)) === 1; + if (isLastDepartmentInUnit) { + throw new Meteor.Error('error-unit-cant-be-empty', "The last department in a unit can't be removed", { + method: 'livechat:saveDepartment', + }); + } + } + + if (!department && !(await isDepartmentCreationAvailable())) { + throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { + method: 'livechat:saveDepartment', + }); + } + + if (department?.archived && departmentData.enabled) { + throw new Meteor.Error('error-archived-department-cant-be-enabled', 'Archived departments cant be enabled', { + method: 'livechat:saveDepartment', + }); + } + + const defaultValidations: Record | BooleanConstructor | StringConstructor> = { + enabled: Boolean, + name: String, + description: Match.Optional(String), + showOnRegistration: Boolean, + email: String, + showOnOfflineForm: Boolean, + requestTagBeforeClosingChat: Match.Optional(Boolean), + chatClosingTags: Match.Optional([String]), + fallbackForwardDepartment: Match.Optional(String), + departmentsAllowedToForward: Match.Optional([String]), + allowReceiveForwardOffline: Match.Optional(Boolean), + }; + + // The Livechat Form department support addition/custom fields, so those fields need to be added before validating + Object.keys(departmentData).forEach((field) => { + if (!defaultValidations.hasOwnProperty(field)) { + defaultValidations[field] = Match.OneOf(String, Match.Integer, Boolean); + } + }); + + check(departmentData, defaultValidations); + check( + departmentAgents, + Match.Maybe({ + upsert: Match.Maybe(Array), + remove: Match.Maybe(Array), + }), + ); + + const { requestTagBeforeClosingChat, chatClosingTags, fallbackForwardDepartment } = departmentData; + if (requestTagBeforeClosingChat && (!chatClosingTags || chatClosingTags.length === 0)) { + throw new Meteor.Error( + 'error-validating-department-chat-closing-tags', + 'At least one closing tag is required when the department requires tag(s) on closing conversations.', + { method: 'livechat:saveDepartment' }, + ); + } + + if (_id && !department) { + throw new Meteor.Error('error-department-not-found', 'Department not found', { + method: 'livechat:saveDepartment', + }); + } + + if (fallbackForwardDepartment === _id) { + throw new Meteor.Error( + 'error-fallback-department-circular', + 'Cannot save department. Circular reference between fallback department and department', + ); + } + + if (fallbackForwardDepartment) { + const fallbackDep = await LivechatDepartment.findOneById(fallbackForwardDepartment, { + projection: { _id: 1, fallbackForwardDepartment: 1 }, + }); + if (!fallbackDep) { + throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', { + method: 'livechat:saveDepartment', + }); + } + } + + const departmentDB = await LivechatDepartment.createOrUpdateDepartment(_id, departmentData); + if (departmentDB && departmentAgents) { + await updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled); + } + + // Disable event + if (department?.enabled && !departmentDB?.enabled) { + await callbacks.run('livechat.afterDepartmentDisabled', departmentDB); + } + + if (departmentUnit) { + await callbacks.run('livechat.manageDepartmentUnit', { userId, departmentId: departmentDB._id, unitId: departmentUnit._id }); + } + + return departmentDB; +} + +export async function archiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById>(_id, { + projection: { _id: 1, businessHourId: 1 }, + }); + + if (!department) { + throw new Error('department-not-found'); + } + + await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]); + + void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); + + await callbacks.run('livechat.afterDepartmentArchived', department); +} + +export async function unarchiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Meteor.Error('department-not-found'); + } + + // TODO: these kind of actions should be on events instead of here + await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); + + void notifyOnLivechatDepartmentAgentChangedByDepartmentId(_id); + + return true; +} + +export async function saveDepartmentAgents( + _id: string, + departmentAgents: { + upsert?: Pick[]; + remove?: Pick[]; + }, +) { + check(_id, String); + check(departmentAgents, { + upsert: Match.Maybe([ + Match.ObjectIncluding({ + agentId: String, + username: String, + count: Match.Maybe(Match.Integer), + order: Match.Maybe(Match.Integer), + }), + ]), + remove: Match.Maybe([ + Match.ObjectIncluding({ + agentId: String, + username: Match.Maybe(String), + count: Match.Maybe(Match.Integer), + order: Match.Maybe(Match.Integer), + }), + ]), + }); + + const department = await LivechatDepartment.findOneById>(_id, { projection: { enabled: 1 } }); + if (!department) { + throw new Meteor.Error('error-department-not-found', 'Department not found'); + } + + return updateDepartmentAgents(_id, departmentAgents, department.enabled); +} diff --git a/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts b/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts new file mode 100644 index 0000000000000..be92a7cfcc545 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts @@ -0,0 +1,24 @@ +import type { ILivechatAgent, SelectedAgent } from '@rocket.chat/core-typings'; +import { Users, LivechatDepartmentAgents } from '@rocket.chat/models'; +import type { FindCursor } from 'mongodb'; + +export async function getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise | undefined> { + if (agent?.agentId) { + return Users.findOnlineAgents(agent.agentId); + } + + if (department) { + const departmentAgents = await LivechatDepartmentAgents.getOnlineForDepartment(department); + if (!departmentAgents) { + return; + } + + const agentIds = await departmentAgents.map(({ agentId }) => agentId).toArray(); + if (!agentIds.length) { + return; + } + + return Users.findByIds([...new Set(agentIds)]); + } + return Users.findOnlineAgents(); +} diff --git a/apps/meteor/app/livechat/server/lib/getRoomMessages.ts b/apps/meteor/app/livechat/server/lib/getRoomMessages.ts new file mode 100644 index 0000000000000..8a9af72b7e23f --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/getRoomMessages.ts @@ -0,0 +1,23 @@ +import type { MessageTypesValues, IRoom } from '@rocket.chat/core-typings'; +import { Rooms, Messages } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +export async function getRoomMessages({ rid }: { rid: string }) { + const room = await Rooms.findOneById>(rid, { projection: { t: 1 } }); + if (room?.t !== 'l') { + throw new Meteor.Error('invalid-room'); + } + + const ignoredMessageTypes: MessageTypesValues[] = [ + 'livechat_navigation_history', + 'livechat_transcript_history', + 'command', + 'livechat-close', + 'livechat-started', + 'livechat_video_call', + ]; + + return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { + sort: { ts: 1 }, + }); +} diff --git a/apps/meteor/app/livechat/server/lib/localTypes.ts b/apps/meteor/app/livechat/server/lib/localTypes.ts index c6acbbc5bcbd6..d58cad8f50bc5 100644 --- a/apps/meteor/app/livechat/server/lib/localTypes.ts +++ b/apps/meteor/app/livechat/server/lib/localTypes.ts @@ -1,4 +1,4 @@ -import type { IOmnichannelRoom, IUser, ILivechatVisitor } from '@rocket.chat/core-typings'; +import type { IOmnichannelRoom, IUser, ILivechatVisitor, IMessage, MessageAttachment, IMessageInbox } from '@rocket.chat/core-typings'; export type GenericCloseRoomParams = { room: IOmnichannelRoom; @@ -29,3 +29,27 @@ export type CloseRoomParamsByVisitor = { } & GenericCloseRoomParams; export type CloseRoomParams = CloseRoomParamsByUser | CloseRoomParamsByVisitor; + +type UploadedFile = { + _id: string; + name?: string; + type?: string; + size?: number; + description?: string; + identify?: { size: { width: number; height: number } }; + format?: string; +}; + +export interface ILivechatMessage { + token: string; + _id: string; + rid: string; + msg: string; + file?: UploadedFile; + files?: UploadedFile[]; + attachments?: MessageAttachment[]; + alias?: string; + groupable?: boolean; + blocks?: IMessage['blocks']; + email?: IMessageInbox['email']; +} diff --git a/apps/meteor/app/livechat/server/methods/saveDepartment.ts b/apps/meteor/app/livechat/server/methods/saveDepartment.ts index 659f85f49945c..484cf275088d7 100644 --- a/apps/meteor/app/livechat/server/methods/saveDepartment.ts +++ b/apps/meteor/app/livechat/server/methods/saveDepartment.ts @@ -3,7 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Livechat } from '../lib/LivechatTyped'; +import { saveDepartment } from '../lib/departmentsLib'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -44,6 +44,6 @@ Meteor.methods({ }); } - return Livechat.saveDepartment(uid, _id, departmentData, { upsert: departmentAgents }, departmentUnit); + return saveDepartment(uid, _id, departmentData, { upsert: departmentAgents }, departmentUnit); }, }); diff --git a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts index 6fac80397906f..742631dcea904 100644 --- a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts +++ b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts @@ -6,7 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings/server'; import { Livechat } from '../lib/LivechatTyped'; -import type { ILivechatMessage } from '../lib/LivechatTyped'; +import type { ILivechatMessage } from '../lib/localTypes'; interface ILivechatMessageAgent { agentId: string; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts index c0f4b1b9da1de..3f0183687e849 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts @@ -1,6 +1,6 @@ import type { IMessage, IOmnichannelRoom } from '@rocket.chat/core-typings'; -import type { CloseRoomParams } from '../../../../../app/livechat/server/lib/LivechatTyped'; +import type { CloseRoomParams } from '../../../../../app/livechat/server/lib/localTypes'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; import { AutoTransferChatScheduler } from '../lib/AutoTransferChatScheduler'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts index ee7cfc6a4c2fe..ee3fa414433c6 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts @@ -2,7 +2,7 @@ import { OmnichannelTranscript } from '@rocket.chat/core-services'; import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; -import type { CloseRoomParams } from '../../../../../app/livechat/server/lib/LivechatTyped'; +import type { CloseRoomParams } from '../../../../../app/livechat/server/lib/localTypes'; import { callbacks } from '../../../../../lib/callbacks'; type LivechatCloseCallbackParams = { diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index f8f5a324a6381..ae2ea140bde19 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -28,7 +28,7 @@ import type { FilterOperators } from 'mongodb'; import type { ILoginAttempt } from '../app/authentication/server/ILoginAttempt'; import type { IBusinessHourBehavior } from '../app/livechat/server/business-hour/AbstractBusinessHour'; -import type { CloseRoomParams } from '../app/livechat/server/lib/LivechatTyped'; +import type { CloseRoomParams } from '../app/livechat/server/lib/localTypes'; import { Callbacks } from './callbacks/callbacksBase'; /**