import type { IOmnichannelRoom, IOmnichannelSystemMessage, IMessage } from '@rocket.chat/core-typings'; import { isEditedMessage, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms, Messages } from '@rocket.chat/models'; import type { Response } from '@rocket.chat/server-fetch'; import { callbacks } from '../../lib/callbacks'; import { getLivechatRoomGuestInfo } from '../../lib/omnichannel/guests'; import { sendRequest } from '../../lib/omnichannel/webhooks'; import { normalizeMessageFileUpload } from '../../lib/utils/functions/normalizeMessageFileUpload'; import { settings } from '../../settings'; type AdditionalFields = | Record | { departmentId: IOmnichannelRoom['departmentId']; } | { departmentId: IOmnichannelRoom['departmentId']; servedBy: IOmnichannelRoom['servedBy']; } | { departmentId: IOmnichannelRoom['departmentId']; servedBy: IOmnichannelRoom['servedBy']; oldDepartmentId: IOmnichannelRoom['departmentId']; oldServedBy: { _id: string; ts: Date; username?: string }; } | { departmentId: IOmnichannelRoom['departmentId']; servedBy: IOmnichannelRoom['servedBy']; closedAt: IOmnichannelRoom['closedAt']; closedBy: IOmnichannelRoom['closedBy']; closer: IOmnichannelRoom['closer']; }; type OmnichannelRoomWithExtraFields = IOmnichannelRoom & { oldServedBy?: { _id: string; ts: Date; username?: string }; oldDepartmentId?: IOmnichannelRoom['departmentId']; }; type CRMActions = | 'LivechatSessionStart' | 'LivechatSessionQueued' | 'LivechatSession' | 'LivechatSessionTaken' | 'LivechatSessionForwarded' | 'LivechatEdit' | 'Message' | 'LeadCapture'; const msgNavType = 'livechat_navigation_history'; const msgClosingType = 'livechat-close'; export const isOmnichannelNavigationMessage = (message: IMessage): message is IOmnichannelSystemMessage => { return message.t === msgNavType; }; export const isOmnichannelClosingMessage = (message: IMessage): message is IOmnichannelSystemMessage => { return message.t === msgClosingType; }; export const sendMessageType = (msgType: string): boolean => { switch (msgType) { case msgClosingType: return true; case msgNavType: return ( settings.get('Livechat_Visitor_navigation_as_a_message') && settings.get('Send_visitor_navigation_history_livechat_webhook_request') ); default: return false; } }; export const getAdditionalFieldsByType = (type: CRMActions, room: OmnichannelRoomWithExtraFields): AdditionalFields => { const { departmentId, servedBy, closedAt, closedBy, closer, oldServedBy, oldDepartmentId } = room; switch (type) { case 'LivechatSessionStart': case 'LivechatSessionQueued': return { departmentId }; case 'LivechatSession': return { departmentId, servedBy, closedAt, closedBy, closer }; case 'LivechatSessionTaken': return { departmentId, servedBy }; case 'LivechatSessionForwarded': return { departmentId, servedBy, oldDepartmentId, oldServedBy }; default: return {}; } }; export async function sendToCRM( type: CRMActions, room: OmnichannelRoomWithExtraFields, includeMessages: boolean | IOmnichannelSystemMessage[] = true, ): Promise { if (!settings.get('Livechat_webhookUrl')) { return room; } const postData: Awaited> & { type: string; messages: IOmnichannelSystemMessage[]; } = { ...(await getLivechatRoomGuestInfo(room)), type, messages: [], }; let messages: IOmnichannelSystemMessage[] | null = null; if (typeof includeMessages === 'boolean' && includeMessages) { messages = await Messages.findVisibleByRoomId(room._id, { sort: { ts: 1 } }).toArray(); } else if (includeMessages instanceof Array) { messages = includeMessages; } if (messages) { for (const message of messages) { if (message.t && !sendMessageType(message.t)) { continue; } const msg = { _id: message._id, username: message.u.username, msg: message.msg || JSON.stringify(message.blocks), ...(message.blocks && message.blocks.length > 0 ? { blocks: message.blocks } : {}), ts: message.ts, rid: message.rid, ...(isEditedMessage(message) && { editedAt: message.editedAt }), ...(message.u.username !== postData.visitor.username && { agentId: message.u._id }), ...(isOmnichannelNavigationMessage(message) && { navigation: message.navigation }), ...(isOmnichannelClosingMessage(message) && { closingMessage: true }), ...(message.file && { file: message.file, attachments: message.attachments }), }; const { u } = message; postData.messages.push({ ...(await normalizeMessageFileUpload({ u, ...msg })), ...{ _updatedAt: message._updatedAt } }); } } const additionalData = getAdditionalFieldsByType(type, room); const responseData = Object.assign(postData, additionalData); // do not wait for the request to be answered // this will avoid blocking the process of saving the message void sendRequest(responseData, 5, async (response: Response) => { if (!response) { return; } const responseData = await response.text(); await LivechatRooms.saveCRMDataByRoomId(room._id, responseData); }); return room; } callbacks.add( 'livechat.closeRoom', async (params) => { const { room } = params; if (!settings.get('Livechat_webhook_on_close')) { return params; } await sendToCRM('LivechatSession', room); return params; }, callbacks.priority.MEDIUM, 'livechat-send-crm-close-room', ); callbacks.add( 'livechat.afterForwardChatToAgent', async (params) => { const { rid, oldServedBy } = params; if (!settings.get('Livechat_webhook_on_forward')) { return params; } const originalRoom = await LivechatRooms.findOneById(rid); if (!originalRoom) { return params; } const room = Object.assign(originalRoom, { oldServedBy }); await sendToCRM('LivechatSessionForwarded', room); return params; }, callbacks.priority.MEDIUM, 'livechat-send-crm-room-forwarded-to-agent', ); callbacks.add( 'livechat.afterForwardChatToDepartment', async (params) => { const { rid, oldDepartmentId } = params; if (!settings.get('Livechat_webhook_on_forward')) { return params; } const originalRoom = await LivechatRooms.findOneById(rid); if (!originalRoom) { return params; } const room = Object.assign(originalRoom, { oldDepartmentId }); await sendToCRM('LivechatSessionForwarded', room); return params; }, callbacks.priority.MEDIUM, 'livechat-send-crm-room-forwarded-to-department', ); callbacks.add( 'livechat.saveInfo', async (room) => { // Do not send to CRM if the chat is still open if (!isOmnichannelRoom(room) || room.open) { return room; } return sendToCRM('LivechatEdit', room); }, callbacks.priority.MEDIUM, 'livechat-send-crm-save-info', ); callbacks.add( 'afterOmnichannelSaveMessage', async (message, { room }) => { // if the message has a token, it was sent from the visitor // if not, it was sent from the agent if (message.token && !settings.get('Livechat_webhook_on_visitor_message')) { return message; } if (!message.token && !settings.get('Livechat_webhook_on_agent_message')) { return message; } // if the message has a type means it is a special message (like the closing comment), so skips // unless the settings that handle with visitor navigation history are enabled if (message.t && !sendMessageType(message.t)) { return message; } await sendToCRM('Message', room, [message]); return message; }, callbacks.priority.MEDIUM, 'livechat-send-crm-message', ); callbacks.add( 'livechat.leadCapture', (room) => { if (!settings.get('Livechat_webhook_on_capture')) { return room; } return sendToCRM('LeadCapture', room, false); }, callbacks.priority.MEDIUM, 'livechat-send-crm-lead-capture', );