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:
41
apps/meteor/server/hooks/omnichannel/afterAgentRemoved.ts
Normal file
41
apps/meteor/server/hooks/omnichannel/afterAgentRemoved.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { LivechatDepartment, Users, LivechatDepartmentAgents, LivechatVisitors } from '@rocket.chat/models';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { notifyOnLivechatDepartmentAgentChanged, notifyOnUserChange } from '../../lib/notifyListener';
|
||||
|
||||
callbacks.add('livechat.afterAgentRemoved', async ({ agent }) => {
|
||||
const departments = await LivechatDepartmentAgents.findByAgentId(agent._id).toArray();
|
||||
|
||||
const [{ modifiedCount }, { deletedCount }] = await Promise.all([
|
||||
Users.removeAgent(agent._id),
|
||||
LivechatDepartmentAgents.removeByAgentId(agent._id),
|
||||
agent.username && LivechatVisitors.removeContactManagerByUsername(agent.username),
|
||||
departments.length && LivechatDepartment.decreaseNumberOfAgentsByIds(departments.map(({ departmentId }) => departmentId)),
|
||||
]);
|
||||
|
||||
if (modifiedCount > 0) {
|
||||
void notifyOnUserChange({
|
||||
id: agent._id,
|
||||
clientAction: 'updated',
|
||||
diff: {
|
||||
operator: false,
|
||||
livechat: null,
|
||||
statusLivechat: null,
|
||||
openBusinessHours: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (deletedCount > 0) {
|
||||
departments.forEach((depAgent) => {
|
||||
void notifyOnLivechatDepartmentAgentChanged(
|
||||
{
|
||||
_id: depAgent._id,
|
||||
agentId: agent._id,
|
||||
departmentId: depAgent.departmentId,
|
||||
},
|
||||
'removed',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
|
||||
import { LivechatRooms } from '@rocket.chat/models';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
|
||||
callbacks.add(
|
||||
'afterSaveMessage',
|
||||
async (message, { room }) => {
|
||||
if (!isOmnichannelRoom(room)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const updater = LivechatRooms.getUpdater();
|
||||
const result = await callbacks.run('afterOmnichannelSaveMessage', message, { room, roomUpdater: updater });
|
||||
|
||||
if (updater.hasChanges()) {
|
||||
await LivechatRooms.updateFromUpdater({ _id: room._id }, updater);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
callbacks.priority.MEDIUM,
|
||||
'after-omnichannel-save-message',
|
||||
);
|
||||
52
apps/meteor/server/hooks/omnichannel/afterUserActions.ts
Normal file
52
apps/meteor/server/hooks/omnichannel/afterUserActions.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { afterAgentUserActivated, afterAgentAdded, afterRemoveAgent } from '../../lib/omnichannel/hooks';
|
||||
|
||||
type IAfterSaveUserProps = {
|
||||
user: IUser;
|
||||
oldUser: IUser | null;
|
||||
};
|
||||
|
||||
const wasAgent = (user: Pick<IUser, 'roles'> | null) => user?.roles?.includes('livechat-agent');
|
||||
const isAgent = (user: Pick<IUser, 'roles'> | null) => user?.roles?.includes('livechat-agent');
|
||||
|
||||
const handleAgentUpdated = async (userData: IAfterSaveUserProps) => {
|
||||
const { user: newUser, oldUser } = userData;
|
||||
|
||||
if (wasAgent(oldUser) && !isAgent(newUser)) {
|
||||
await afterRemoveAgent(newUser);
|
||||
}
|
||||
|
||||
if (!wasAgent(oldUser) && isAgent(newUser)) {
|
||||
await afterAgentAdded(newUser);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAgentCreated = async (user: IUser) => {
|
||||
// created === no prev roles :)
|
||||
if (isAgent(user)) {
|
||||
await afterAgentAdded(user);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeactivateUser = async (user: IUser) => {
|
||||
if (wasAgent(user)) {
|
||||
await Users.makeAgentUnavailable(user._id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivateUser = async (user: IUser) => {
|
||||
if (isAgent(user) && user.username) {
|
||||
await afterAgentUserActivated(user);
|
||||
}
|
||||
};
|
||||
|
||||
callbacks.add('afterCreateUser', handleAgentCreated, callbacks.priority.LOW, 'livechat-after-create-user-update-agent');
|
||||
|
||||
callbacks.add('afterSaveUser', handleAgentUpdated, callbacks.priority.LOW, 'livechat-after-save-user-update-agent');
|
||||
|
||||
callbacks.add('afterDeactivateUser', handleDeactivateUser, callbacks.priority.LOW, 'livechat-after-deactivate-user-remove-agent');
|
||||
|
||||
callbacks.add('afterActivateUser', handleActivateUser, callbacks.priority.LOW, 'livechat-after-activate-user-add-agent');
|
||||
69
apps/meteor/server/hooks/omnichannel/leadCapture.ts
Normal file
69
apps/meteor/server/hooks/omnichannel/leadCapture.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { IMessage, IOmnichannelRoom } from '@rocket.chat/core-typings';
|
||||
import { isEditedMessage } from '@rocket.chat/core-typings';
|
||||
import { LivechatVisitors } from '@rocket.chat/models';
|
||||
import { isTruthy } from '@rocket.chat/tools';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
function validateMessage(message: IMessage, room: IOmnichannelRoom) {
|
||||
// skips this callback if the message was edited
|
||||
if (isEditedMessage(message)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// message valid only if it is a livechat room
|
||||
if (!(typeof room.t !== 'undefined' && room.t === 'l' && room.v && room.v.token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the message hasn't a token, it was NOT sent from the visitor, so ignore it
|
||||
if (!message.token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the message has a type means it is a special message (like the closing comment), so skips
|
||||
if (message.t) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
callbacks.add(
|
||||
'afterOmnichannelSaveMessage',
|
||||
async (message, { room }) => {
|
||||
if (!validateMessage(message, room)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const phoneRegexSetting = settings.get<string>('Livechat_lead_phone_regex');
|
||||
const emailRegexSetting = settings.get<string>('Livechat_lead_email_regex');
|
||||
|
||||
const safeMatch = (pattern: string, flags: string, text: string): string[] => {
|
||||
if (!pattern) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const re = new RegExp(pattern, flags);
|
||||
return text.match(re)?.filter(isTruthy) ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const uniq = (arr: string[]) => [...new Set(arr.filter(isTruthy))];
|
||||
|
||||
const matchedPhones = uniq(safeMatch(phoneRegexSetting, 'g', message.msg));
|
||||
const matchedEmails = uniq(safeMatch(emailRegexSetting, 'gi', message.msg));
|
||||
|
||||
if (matchedEmails.length || matchedPhones.length) {
|
||||
await LivechatVisitors.saveGuestEmailPhoneById(room.v._id, matchedEmails, matchedPhones);
|
||||
await callbacks.run('livechat.leadCapture', room);
|
||||
}
|
||||
|
||||
return message;
|
||||
},
|
||||
callbacks.priority.LOW,
|
||||
'leadCapture',
|
||||
);
|
||||
30
apps/meteor/server/hooks/omnichannel/markRoomNotResponded.ts
Normal file
30
apps/meteor/server/hooks/omnichannel/markRoomNotResponded.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { isEditedMessage } from '@rocket.chat/core-typings';
|
||||
import { LivechatRooms } from '@rocket.chat/models';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
|
||||
callbacks.add(
|
||||
'afterOmnichannelSaveMessage',
|
||||
(message, { room, roomUpdater }) => {
|
||||
// skips this callback if the message was edited
|
||||
if (!message || isEditedMessage(message)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
// if the message has not a token, it was sent by the agent, so ignore it
|
||||
if (!message.token) {
|
||||
return message;
|
||||
}
|
||||
|
||||
// check if room is yet awaiting for response
|
||||
if (room.waitingResponse) {
|
||||
return message;
|
||||
}
|
||||
|
||||
LivechatRooms.getNotResponseByRoomIdUpdateQuery(roomUpdater);
|
||||
|
||||
return message;
|
||||
},
|
||||
callbacks.priority.LOW,
|
||||
'markRoomNotResponded',
|
||||
);
|
||||
79
apps/meteor/server/hooks/omnichannel/markRoomResponded.ts
Normal file
79
apps/meteor/server/hooks/omnichannel/markRoomResponded.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { IOmnichannelRoom, IMessage } from '@rocket.chat/core-typings';
|
||||
import { isEditedMessage, isMessageFromVisitor, isSystemMessage } from '@rocket.chat/core-typings';
|
||||
import type { Updater } from '@rocket.chat/models';
|
||||
import { LivechatRooms, LivechatContacts, LivechatInquiry } from '@rocket.chat/models';
|
||||
import moment from 'moment';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { notifyOnLivechatInquiryChanged } from '../../lib/notifyListener';
|
||||
import { isMessageFromBot } from '../../lib/omnichannel/isMessageFromBot';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export async function markRoomResponded(
|
||||
message: IMessage,
|
||||
room: IOmnichannelRoom,
|
||||
roomUpdater: Updater<IOmnichannelRoom>,
|
||||
): Promise<IOmnichannelRoom['responseBy'] | undefined> {
|
||||
if (
|
||||
isSystemMessage(message) ||
|
||||
isEditedMessage(message) ||
|
||||
isMessageFromVisitor(message) ||
|
||||
(settings.get<boolean>('Omnichannel_Metrics_Ignore_Automatic_Messages') && (await isMessageFromBot(message)))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const monthYear = moment().format('YYYY-MM');
|
||||
const isContactActive = await LivechatContacts.isContactActiveOnPeriod({ visitorId: room.v._id, source: room.source }, monthYear);
|
||||
|
||||
// Case: agent answers & visitor is not active, we mark visitor as active
|
||||
if (!isContactActive) {
|
||||
await LivechatContacts.markContactActiveForPeriod({ visitorId: room.v._id, source: room.source }, monthYear);
|
||||
}
|
||||
|
||||
if (!room.v?.activity?.includes(monthYear)) {
|
||||
LivechatRooms.getVisitorActiveForPeriodUpdateQuery(monthYear, roomUpdater, room.v?.activity);
|
||||
const livechatInquiry = await LivechatInquiry.markInquiryActiveForPeriod(room._id, monthYear);
|
||||
|
||||
if (livechatInquiry) {
|
||||
void notifyOnLivechatInquiryChanged(livechatInquiry, 'updated', { v: livechatInquiry.v });
|
||||
}
|
||||
}
|
||||
|
||||
if (!room.waitingResponse) {
|
||||
// case where agent sends second message or any subsequent message in a room before visitor responds to the first message
|
||||
// in this case, we just need to update the lastMessageTs of the responseBy object
|
||||
if (room.responseBy) {
|
||||
LivechatRooms.getAgentLastMessageTsUpdateQuery(roomUpdater);
|
||||
}
|
||||
|
||||
return room.responseBy;
|
||||
}
|
||||
|
||||
// Since we're updating the whole object anyways, we re-use the same values from object (or from message if not present)
|
||||
// And then we update the lastMessageTs, which is the only thing that should be updating here
|
||||
const { responseBy: { _id, username, firstResponseTs } = {} } = room;
|
||||
const responseBy: IOmnichannelRoom['responseBy'] = {
|
||||
_id: _id || message.u._id,
|
||||
username: username || message.u.username,
|
||||
firstResponseTs: firstResponseTs || new Date(message.ts),
|
||||
lastMessageTs: new Date(message.ts),
|
||||
};
|
||||
|
||||
LivechatRooms.getResponseByRoomIdUpdateQuery(responseBy, roomUpdater);
|
||||
|
||||
return responseBy;
|
||||
}
|
||||
|
||||
callbacks.add(
|
||||
'afterOmnichannelSaveMessage',
|
||||
async (message, { room, roomUpdater }) => {
|
||||
if (!message || isEditedMessage(message) || isMessageFromVisitor(message) || isSystemMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await markRoomResponded(message, room, roomUpdater);
|
||||
},
|
||||
callbacks.priority.HIGH,
|
||||
'markRoomResponded',
|
||||
);
|
||||
26
apps/meteor/server/hooks/omnichannel/offlineMessage.ts
Normal file
26
apps/meteor/server/hooks/omnichannel/offlineMessage.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { sendRequest } from '../../lib/omnichannel/webhooks';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
callbacks.add(
|
||||
'livechat.offlineMessage',
|
||||
async (data) => {
|
||||
if (!settings.get('Livechat_webhook_on_offline_msg')) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const postData = {
|
||||
type: 'LivechatOfflineMessage',
|
||||
sentAt: new Date(),
|
||||
visitor: {
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
},
|
||||
message: data.message,
|
||||
};
|
||||
|
||||
await sendRequest(postData);
|
||||
},
|
||||
callbacks.priority.MEDIUM,
|
||||
'livechat-send-email-offline-message',
|
||||
);
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
|
||||
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
|
||||
import { LivechatDepartment, Users, Rooms } from '@rocket.chat/models';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { i18n } from '../../lib/i18n';
|
||||
import { sendMessage } from '../../lib/messages/sendMessage';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
callbacks.add(
|
||||
'livechat.offlineMessage',
|
||||
async (data) => {
|
||||
if (!settings.get('Livechat_OfflineMessageToChannel_enabled')) {
|
||||
return data;
|
||||
}
|
||||
|
||||
let channelName = settings.get<string>('Livechat_OfflineMessageToChannel_channel_name');
|
||||
let departmentName;
|
||||
const { name, email, department, message: text, host } = data;
|
||||
if (department && department !== '') {
|
||||
const dept = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'name' | 'offlineMessageChannelName'>>(
|
||||
department,
|
||||
{
|
||||
projection: { name: 1, offlineMessageChannelName: 1 },
|
||||
},
|
||||
);
|
||||
departmentName = dept?.name;
|
||||
if (dept?.offlineMessageChannelName) {
|
||||
channelName = dept.offlineMessageChannelName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!channelName || channelName === '') {
|
||||
return data;
|
||||
}
|
||||
|
||||
const room = await Rooms.findOneByName(channelName, { projection: { t: 1, archived: 1 } });
|
||||
if (!room || room.archived || (isOmnichannelRoom(room) && room.closedAt)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const user = await Users.findOneById('rocket.cat', { projection: { username: 1 } });
|
||||
if (!user) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const lng = settings.get<string>('Language') || 'en';
|
||||
|
||||
let msg = `${i18n.t('New_Livechat_offline_message_has_been_sent', { lng })}: \n`;
|
||||
if (host && host !== '') {
|
||||
msg = msg.concat(`${i18n.t('Sent_from', { lng })}: ${host} \n`);
|
||||
}
|
||||
msg = msg.concat(`${i18n.t('Visitor_Name', { lng })}: ${name} \n`);
|
||||
msg = msg.concat(`${i18n.t('Visitor_Email', { lng })}: ${email} \n`);
|
||||
if (departmentName) {
|
||||
msg = msg.concat(`${i18n.t('Department', { lng })}: ${departmentName} \n`);
|
||||
}
|
||||
msg = msg.concat(`${i18n.t('Message', { lng })}: ${text} \n`);
|
||||
|
||||
const message = {
|
||||
rid: room._id,
|
||||
msg,
|
||||
groupable: false,
|
||||
};
|
||||
|
||||
await sendMessage(user, message, room, { upsert: true });
|
||||
},
|
||||
callbacks.priority.MEDIUM,
|
||||
'livechat-send-email-offline-message-to-channel',
|
||||
);
|
||||
137
apps/meteor/server/hooks/omnichannel/processRoomAbandonment.ts
Normal file
137
apps/meteor/server/hooks/omnichannel/processRoomAbandonment.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import type { IOmnichannelRoom, IMessage, IBusinessHourWorkHour, ILivechatDepartment } from '@rocket.chat/core-typings';
|
||||
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
|
||||
import { LivechatBusinessHours, LivechatDepartment, Messages, LivechatRooms } from '@rocket.chat/models';
|
||||
import moment from 'moment';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { businessHourManager } from '../../lib/omnichannel/business-hour';
|
||||
import type { CloseRoomParams } from '../../lib/omnichannel/localTypes';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export const getSecondsWhenOfficeHoursIsDisabled = (room: IOmnichannelRoom, agentLastMessage: IMessage) =>
|
||||
moment(new Date(room.closedAt || new Date())).diff(moment(new Date(agentLastMessage.ts)), 'seconds');
|
||||
|
||||
export const parseDays = (
|
||||
acc: Record<string, { start: { day: string; time: string }; finish: { day: string; time: string }; open: boolean }>,
|
||||
day: IBusinessHourWorkHour,
|
||||
) => {
|
||||
acc[day.day] = {
|
||||
start: { day: day.start.utc.dayOfWeek, time: day.start.utc.time },
|
||||
finish: { day: day.finish.utc.dayOfWeek, time: day.finish.utc.time },
|
||||
open: day.open,
|
||||
};
|
||||
return acc;
|
||||
};
|
||||
|
||||
export const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLastMessage: IMessage) => {
|
||||
if (!settings.get('Livechat_enable_business_hours')) {
|
||||
return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
|
||||
}
|
||||
let officeDays;
|
||||
const department = room.departmentId
|
||||
? await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'businessHourId'>>(room.departmentId, {
|
||||
projection: { businessHourId: 1 },
|
||||
})
|
||||
: null;
|
||||
if (department?.businessHourId) {
|
||||
const businessHour = await LivechatBusinessHours.findOneById(department.businessHourId);
|
||||
if (!businessHour) {
|
||||
return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
|
||||
}
|
||||
|
||||
officeDays = (await businessHourManager.getBusinessHour(businessHour._id, businessHour.type))?.workHours.reduce(parseDays, {});
|
||||
} else {
|
||||
officeDays = (await businessHourManager.getBusinessHour())?.workHours.reduce(parseDays, {});
|
||||
}
|
||||
|
||||
// Empty object we assume invalid config
|
||||
if (!officeDays || !Object.keys(officeDays).length) {
|
||||
return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
|
||||
}
|
||||
|
||||
let totalSeconds = 0;
|
||||
const endOfConversation = moment.utc(new Date(room.closedAt || new Date()));
|
||||
const startOfInactivity = moment.utc(new Date(agentLastMessage.ts));
|
||||
const daysOfInactivity = endOfConversation.clone().startOf('day').diff(startOfInactivity.clone().startOf('day'), 'days');
|
||||
const inactivityDay = moment.utc(new Date(agentLastMessage.ts));
|
||||
|
||||
for (let index = 0; index <= daysOfInactivity; index++) {
|
||||
const today = inactivityDay.clone().format('dddd');
|
||||
const officeDay = officeDays[today];
|
||||
|
||||
if (!officeDay) {
|
||||
inactivityDay.add(1, 'days');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!officeDay.open) {
|
||||
inactivityDay.add(1, 'days');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!officeDay?.start?.time || !officeDay?.finish?.time) {
|
||||
inactivityDay.add(1, 'days');
|
||||
continue;
|
||||
}
|
||||
|
||||
const [officeStartHour, officeStartMinute] = officeDay.start.time.split(':');
|
||||
const [officeCloseHour, officeCloseMinute] = officeDay.finish.time.split(':');
|
||||
// We should only take in consideration the time where the office is open and the conversation was inactive
|
||||
const todayStartOfficeHours = inactivityDay
|
||||
.clone()
|
||||
.set({ hour: parseInt(officeStartHour, 10), minute: parseInt(officeStartMinute, 10) });
|
||||
const todayEndOfficeHours = inactivityDay.clone().set({ hour: parseInt(officeCloseHour, 10), minute: parseInt(officeCloseMinute, 10) });
|
||||
|
||||
// 1: Room was inactive the whole day, we add the whole time BH is inactive
|
||||
if (startOfInactivity.isBefore(todayStartOfficeHours) && endOfConversation.isAfter(todayEndOfficeHours)) {
|
||||
totalSeconds += todayEndOfficeHours.diff(todayStartOfficeHours, 'seconds');
|
||||
}
|
||||
|
||||
// 2: Room was inactive before start but was closed before end of BH, we add the inactive time
|
||||
if (startOfInactivity.isBefore(todayStartOfficeHours) && endOfConversation.isBefore(todayEndOfficeHours)) {
|
||||
totalSeconds += endOfConversation.diff(todayStartOfficeHours, 'seconds');
|
||||
}
|
||||
|
||||
// 3: Room was inactive after start and ended after end of BH, we add the inactive time
|
||||
if (startOfInactivity.isAfter(todayStartOfficeHours) && endOfConversation.isAfter(todayEndOfficeHours)) {
|
||||
totalSeconds += todayEndOfficeHours.diff(startOfInactivity, 'seconds');
|
||||
}
|
||||
|
||||
// 4: Room was inactive after start and before end of BH, we add the inactive time
|
||||
if (startOfInactivity.isAfter(todayStartOfficeHours) && endOfConversation.isBefore(todayEndOfficeHours)) {
|
||||
totalSeconds += endOfConversation.diff(startOfInactivity, 'seconds');
|
||||
}
|
||||
|
||||
inactivityDay.add(1, 'days');
|
||||
}
|
||||
return totalSeconds;
|
||||
};
|
||||
|
||||
export const onCloseRoom = async (params: { room: IOmnichannelRoom; options: CloseRoomParams['options'] }) => {
|
||||
const { room } = params;
|
||||
|
||||
if (!isOmnichannelRoom(room)) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const closedByAgent = room.closer !== 'visitor';
|
||||
const wasTheLastMessageSentByAgent = room.lastMessage && !room.lastMessage.token;
|
||||
if (!closedByAgent || !wasTheLastMessageSentByAgent) {
|
||||
return params;
|
||||
}
|
||||
|
||||
if (!room.v?.lastMessageTs) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const agentLastMessage = await Messages.findAgentLastMessageByVisitorLastMessageTs(room._id, room.v.lastMessageTs);
|
||||
if (!agentLastMessage) {
|
||||
return params;
|
||||
}
|
||||
const secondsSinceLastAgentResponse = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
|
||||
await LivechatRooms.setVisitorInactivityInSecondsById(room._id, secondsSinceLastAgentResponse);
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
callbacks.add('livechat.closeRoom', onCloseRoom, callbacks.priority.HIGH, 'process-room-abandonment');
|
||||
90
apps/meteor/server/hooks/omnichannel/saveAnalyticsData.ts
Normal file
90
apps/meteor/server/hooks/omnichannel/saveAnalyticsData.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { isEditedMessage, isMessageFromVisitor, isSystemMessage } from '@rocket.chat/core-typings';
|
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
|
||||
import { LivechatRooms } from '@rocket.chat/models';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { isMessageFromBot } from '../../lib/omnichannel/isMessageFromBot';
|
||||
import { normalizeMessageFileUpload } from '../../lib/utils/functions/normalizeMessageFileUpload';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
const getMetricValue = <T>(metric: T | undefined, defaultValue: T): T => metric ?? defaultValue;
|
||||
const calculateTimeDifference = <T extends Date | number>(startTime: T, now: Date): number =>
|
||||
(now.getTime() - new Date(startTime).getTime()) / 1000;
|
||||
const calculateAvgResponseTime = (totalResponseTime: number, newResponseTime: number, responseCount: number) =>
|
||||
(totalResponseTime + newResponseTime) / (responseCount + 1);
|
||||
|
||||
const getFirstResponseAnalytics = (
|
||||
visitorLastQuery: Date,
|
||||
agentJoinTime: Date,
|
||||
totalResponseTime: number,
|
||||
responseCount: number,
|
||||
now: Date,
|
||||
) => {
|
||||
const responseTime = calculateTimeDifference(visitorLastQuery, now);
|
||||
const reactionTime = calculateTimeDifference(agentJoinTime, now);
|
||||
const avgResponseTime = calculateAvgResponseTime(totalResponseTime, responseTime, responseCount);
|
||||
|
||||
return {
|
||||
firstResponseDate: now,
|
||||
firstResponseTime: responseTime,
|
||||
responseTime,
|
||||
avgResponseTime,
|
||||
firstReactionDate: now,
|
||||
firstReactionTime: reactionTime,
|
||||
reactionTime,
|
||||
};
|
||||
};
|
||||
|
||||
const getSubsequentResponseAnalytics = (visitorLastQuery: Date, totalResponseTime: number, responseCount: number, now: Date) => {
|
||||
const responseTime = calculateTimeDifference(visitorLastQuery, now);
|
||||
const avgResponseTime = calculateAvgResponseTime(totalResponseTime, responseTime, responseCount);
|
||||
|
||||
return {
|
||||
responseTime,
|
||||
avgResponseTime,
|
||||
reactionTime: responseTime,
|
||||
};
|
||||
};
|
||||
|
||||
const getAnalyticsData = (room: IOmnichannelRoom, now: Date): Record<string, string | number | Date> | undefined => {
|
||||
const visitorLastQuery = getMetricValue(room.metrics?.v?.lq, room.ts);
|
||||
const agentLastReply = getMetricValue(room.metrics?.servedBy?.lr, room.ts);
|
||||
const agentJoinTime = getMetricValue(room.servedBy?.ts, room.ts);
|
||||
const totalResponseTime = getMetricValue(room.metrics?.response?.tt, 0);
|
||||
const responseCount = getMetricValue(room.metrics?.response?.total, 0);
|
||||
|
||||
if (agentLastReply === room.ts) {
|
||||
return getFirstResponseAnalytics(visitorLastQuery, agentJoinTime, totalResponseTime, responseCount, now);
|
||||
}
|
||||
if (visitorLastQuery > agentLastReply) {
|
||||
return getSubsequentResponseAnalytics(visitorLastQuery, totalResponseTime, responseCount, now);
|
||||
}
|
||||
};
|
||||
|
||||
callbacks.add(
|
||||
'afterOmnichannelSaveMessage',
|
||||
async (message, { room, roomUpdater }) => {
|
||||
if (!message || isEditedMessage(message) || isSystemMessage(message)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (message.file) {
|
||||
message = { ...(await normalizeMessageFileUpload(message)), ...{ _updatedAt: message._updatedAt } };
|
||||
}
|
||||
|
||||
if (isMessageFromVisitor(message)) {
|
||||
LivechatRooms.getAnalyticsUpdateQueryBySentByVisitor(room, message, roomUpdater);
|
||||
} else {
|
||||
if (settings.get<boolean>('Omnichannel_Metrics_Ignore_Automatic_Messages') && (await isMessageFromBot(message))) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const analyticsData = getAnalyticsData(room, new Date());
|
||||
LivechatRooms.getAnalyticsUpdateQueryBySentByAgent(room, message, analyticsData, roomUpdater);
|
||||
}
|
||||
|
||||
return message;
|
||||
},
|
||||
callbacks.priority.LOW,
|
||||
'saveAnalyticsData',
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
import { isEditedMessage } from '@rocket.chat/core-typings';
|
||||
import { LivechatInquiry } from '@rocket.chat/models';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { notifyOnLivechatInquiryChanged } from '../../lib/notifyListener';
|
||||
import { RoutingManager } from '../../lib/omnichannel/RoutingManager';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
callbacks.add(
|
||||
'afterOmnichannelSaveMessage',
|
||||
async (message, { room }) => {
|
||||
if (isEditedMessage(message) || message.t) {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (!RoutingManager.getConfig()?.showQueue) {
|
||||
// since last message is only getting used on UI as preview message when queue is enabled
|
||||
return message;
|
||||
}
|
||||
|
||||
if (!settings.get('Store_Last_Message')) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const livechatInquiry = await LivechatInquiry.setLastMessageByRoomId(room._id, message);
|
||||
if (livechatInquiry) {
|
||||
void notifyOnLivechatInquiryChanged(livechatInquiry, 'updated', { lastMessage: message });
|
||||
}
|
||||
|
||||
return message;
|
||||
},
|
||||
callbacks.priority.LOW,
|
||||
'save-last-message-to-inquiry',
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { isMessageFromVisitor } from '@rocket.chat/core-typings';
|
||||
import { LivechatRooms } from '@rocket.chat/models';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
|
||||
callbacks.add(
|
||||
'afterOmnichannelSaveMessage',
|
||||
async (message, { roomUpdater }) => {
|
||||
if (message.t || !isMessageFromVisitor(message)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
await LivechatRooms.getVisitorLastMessageTsUpdateQueryByRoomId(message.ts, roomUpdater);
|
||||
|
||||
return message;
|
||||
},
|
||||
callbacks.priority.HIGH,
|
||||
'save-last-visitor-message-timestamp',
|
||||
);
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
|
||||
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
|
||||
import { LivechatRooms } from '@rocket.chat/models';
|
||||
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import type { CloseRoomParams } from '../../lib/omnichannel/localTypes';
|
||||
import { sendTranscript } from '../../lib/omnichannel/sendTranscript';
|
||||
|
||||
type LivechatCloseCallbackParams = {
|
||||
room: IOmnichannelRoom;
|
||||
options: CloseRoomParams['options'];
|
||||
};
|
||||
|
||||
const sendEmailTranscriptOnClose = async (params: LivechatCloseCallbackParams): Promise<LivechatCloseCallbackParams> => {
|
||||
const { room, options } = params;
|
||||
|
||||
if (!isOmnichannelRoom(room)) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const { _id: rid, v: { token } = {} } = room;
|
||||
if (!token) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const transcriptData = resolveTranscriptData(room, options);
|
||||
if (!transcriptData) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const { email, subject, requestedBy: user } = transcriptData;
|
||||
|
||||
await Promise.all([sendTranscript({ token, rid, email, subject, user }), LivechatRooms.unsetEmailTranscriptRequestedByRoomId(rid)]);
|
||||
|
||||
delete room.transcriptRequest;
|
||||
|
||||
return {
|
||||
room,
|
||||
options,
|
||||
};
|
||||
};
|
||||
|
||||
const resolveTranscriptData = (
|
||||
room: IOmnichannelRoom,
|
||||
options: LivechatCloseCallbackParams['options'] = {},
|
||||
): IOmnichannelRoom['transcriptRequest'] | undefined => {
|
||||
const { transcriptRequest: roomTranscriptRequest } = room;
|
||||
|
||||
const { emailTranscript: optionsTranscriptRequest } = options;
|
||||
|
||||
// Note: options.emailTranscript will override the room.transcriptRequest check
|
||||
// If options.emailTranscript is not set, then the room.transcriptRequest will be checked
|
||||
if (optionsTranscriptRequest === undefined) {
|
||||
return roomTranscriptRequest;
|
||||
}
|
||||
|
||||
if (!optionsTranscriptRequest.sendToVisitor) {
|
||||
return undefined;
|
||||
}
|
||||
return optionsTranscriptRequest.requestData;
|
||||
};
|
||||
|
||||
callbacks.add('livechat.closeRoom', sendEmailTranscriptOnClose, callbacks.priority.HIGH, 'livechat-send-email-transcript-on-close-room');
|
||||
263
apps/meteor/server/hooks/omnichannel/sendToCRM.ts
Normal file
263
apps/meteor/server/hooks/omnichannel/sendToCRM.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
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<string, unknown>
|
||||
| {
|
||||
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<boolean>('Livechat_Visitor_navigation_as_a_message') &&
|
||||
settings.get<boolean>('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<OmnichannelRoomWithExtraFields> {
|
||||
if (!settings.get('Livechat_webhookUrl')) {
|
||||
return room;
|
||||
}
|
||||
|
||||
const postData: Awaited<ReturnType<typeof getLivechatRoomGuestInfo>> & {
|
||||
type: string;
|
||||
messages: IOmnichannelSystemMessage[];
|
||||
} = {
|
||||
...(await getLivechatRoomGuestInfo(room)),
|
||||
type,
|
||||
messages: [],
|
||||
};
|
||||
|
||||
let messages: IOmnichannelSystemMessage[] | null = null;
|
||||
if (typeof includeMessages === 'boolean' && includeMessages) {
|
||||
messages = await Messages.findVisibleByRoomId<IOmnichannelSystemMessage>(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',
|
||||
);
|
||||
Reference in New Issue
Block a user