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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/livechat/server/hooks/markRoomNotResponded.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

import { callbacks } from '../../../callbacks';
import { LivechatRooms } from '../../../models';

callbacks.add('afterSaveMessage', function(message, room) {
// skips this callback if the message was edited
if (!message || message.editedAt) {
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 (typeof room.t !== 'undefined' && room.t === 'l' && room.waitingResponse) {
return message;
}

LivechatRooms.setNotResponseByRoomId(room._id);

return message;
}, callbacks.priority.LOW, 'markRoomNotResponded');
2 changes: 1 addition & 1 deletion app/livechat/server/hooks/processRoomAbandonment.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@ callbacks.add('livechat.closeRoom', (room) => {
return;
}
const secondsSinceLastAgentResponse = getSecondsSinceLastAgentResponse(room, agentLastMessage);
LivechatRooms.setVisitorInactivityInSecondsByRoomId(room._id, secondsSinceLastAgentResponse);
LivechatRooms.setVisitorInactivityInSecondsById(room._id, secondsSinceLastAgentResponse);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This method needs to be renamed on the Model as well.
The old name is still there.

}, callbacks.priority.HIGH, 'process-room-abandonment');
1 change: 1 addition & 0 deletions app/livechat/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import './hooks/sendToCRM';
import './hooks/sendToFacebook';
import './hooks/processRoomAbandonment';
import './hooks/saveLastVisitorMessageTs';
import './hooks/markRoomNotResponded';
import './methods/addAgent';
Comment thread
MarcosSpessatto marked this conversation as resolved.
import './methods/addManager';
import './methods/changeLivechatStatus';
Expand Down
17 changes: 16 additions & 1 deletion app/models/server/models/LivechatRooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class LivechatRooms extends Base {
this.tryEnsureIndex({ 'metrics.chatDuration': 1 }, { sparse: true });
this.tryEnsureIndex({ 'metrics.serviceTimeDuration': 1 }, { sparse: true });
this.tryEnsureIndex({ 'metrics.visitorInactivity': 1 }, { sparse: true });
this.tryEnsureIndex({ 'omnichannel.predictedVisitorAbandonmentAt': 1 }, { sparse: true });
}

findLivechat(filter = {}, offset = 0, limit = 20) {
Expand Down Expand Up @@ -286,6 +287,20 @@ export class LivechatRooms extends Base {
});
}

setNotResponseByRoomId(roomId) {
return this.update({
_id: roomId,
t: 'l',
}, {
$set: {
waitingResponse: true,
},
$unset: {
responseBy: 1,
},
});
}

setAgentLastMessageTs(roomId) {
return this.update({
_id: roomId,
Expand Down Expand Up @@ -566,7 +581,7 @@ export class LivechatRooms extends Base {
return this.update(query, update);
}

setVisitorInactivityInSecondsByRoomId(roomId, visitorInactivity) {
setVisitorInactivityInSecondsById(roomId, visitorInactivity) {
const query = {
_id: roomId,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
<input type="number" class="rc-input__element customFormField" name="maxNumberSimultaneousChat" value="{{department.maxNumberSimultaneousChat}}" placeholder="{{_ "Max_number_of_chats_per_agent"}}" />
</div>
</div>
<div class="input-line">
<label>{{_ "How_long_to_wait_to_consider_visitor_abandonment"}}</label>
<div>
<input type="number" class="rc-input__element customFormField" name="visitorInactivityTimeoutInSeconds" value="{{department.visitorInactivityTimeoutInSeconds}}" placeholder="{{_ "Number_in_seconds"}}" />
</div>
</div>
<div class="input-line">
<label>{{_ "Livechat_abandoned_rooms_closed_custom_message"}}</label>
<div>
<input type="text" class="rc-input__element customFormField" name="abandonedRoomsCloseCustomMessage" value="{{department.abandonedRoomsCloseCustomMessage}}" placeholder="{{_ "Enter_a_custom_message"}}" />
</div>
</div>
<div class="input-line">
<label>{{_ "Waiting_queue_message"}}</label>
<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { callbacks } from '../../../../../app/callbacks/server';
import LivechatRooms from '../../../../../app/models/server/models/LivechatRooms';
import LivechatDepartment from '../../../../../app/models/server/models/LivechatDepartment';
import { setPredictedVisitorAbandonmentTime } from '../lib/Helper';

callbacks.add('livechat.afterForwardChatToDepartment', (options) => {
const { rid, newDepartmentId } = options;
Expand All @@ -9,6 +10,7 @@ callbacks.add('livechat.afterForwardChatToDepartment', (options) => {
if (!room) {
return;
}
setPredictedVisitorAbandonmentTime(room);

const department = LivechatDepartment.findOneById(newDepartmentId, { fields: { ancestors: 1 } });
if (!department) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { callbacks } from '../../../../../app/callbacks/server';
import { settings } from '../../../../../app/settings/server';
import { setPredictedVisitorAbandonmentTime } from '../lib/Helper';

callbacks.add('afterSaveMessage', function(message, room) {
if (!settings.get('Livechat_auto_close_abandoned_rooms') || settings.get('Livechat_visitor_inactivity_timeout') <= 0) {
return message;
}
// skips this callback if the message was edited
if (message.editedAt) {
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 has a type means it is a special message (like the closing comment), so skips
if (message.t) {
return false;
}
const sentByAgent = !message.token;
if (sentByAgent) {
setPredictedVisitorAbandonmentTime(room);
}
return message;
}, callbacks.priority.HIGH, 'save-visitor-inactivity');
1 change: 1 addition & 0 deletions ee/app/livechat-enterprise/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor';
import './hooks/addDepartmentAncestors';
import './hooks/afterForwardChatToDepartment';
import './hooks/beforeListTags';
import './hooks/setPredictedVisitorAbandonmentTime';
import './methods/addMonitor';
import './methods/getUnitsFromUserRoles';
import './methods/removeMonitor';
Expand Down
33 changes: 31 additions & 2 deletions ee/app/livechat-enterprise/server/lib/Helper.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import moment from 'moment';

import { hasRole } from '../../../../../app/authorization';
import { LivechatDepartment, Users, LivechatInquiry } from '../../../../../app/models/server';
import { LivechatDepartment, Users, LivechatInquiry, LivechatRooms } from '../../../../../app/models/server';
import { Rooms as RoomRaw } from '../../../../../app/models/server/raw';
import { settings } from '../../../../../app/settings';
import { Livechat } from '../../../../../app/livechat/server/lib/Livechat';
Expand Down Expand Up @@ -64,7 +65,7 @@ export const normalizeQueueInfo = async ({ position, queueInfo, department }) =>
queueInfo = await getQueueInfo(department);
}

const { message, numberMostRecentChats, statistics: { avgChatDuration } = { } } = queueInfo;
const { message, numberMostRecentChats, statistics: { avgChatDuration } = {} } = queueInfo;
const spot = position + 1;
const estimatedWaitTimeSeconds = getSpotEstimatedWaitTime(spot, numberMostRecentChats, avgChatDuration);
return { spot, message, estimatedWaitTimeSeconds };
Expand Down Expand Up @@ -137,3 +138,31 @@ export const allowAgentSkipQueue = (agent) => {

return settings.get('Livechat_assign_new_conversation_to_bot') && hasRole(agent.agentId, 'bot');
};

export const setPredictedVisitorAbandonmentTime = (room) => {
if (!room.v || !room.v.lastMessageTs || !settings.get('Livechat_auto_close_abandoned_rooms')) {
return;
}

let secondsToAdd = settings.get('Livechat_visitor_inactivity_timeout');

const department = room.departmentId && LivechatDepartment.findOneById(room.departmentId);
if (department && department.visitorInactivityTimeoutInSeconds) {
secondsToAdd = department.visitorInactivityTimeoutInSeconds;
}

if (secondsToAdd <= 0) {
return;
}

const willBeAbandonedAt = moment(room.v.lastMessageTs).add(Number(secondsToAdd), 'seconds').toDate();
LivechatRooms.setPredictedVisitorAbandonment(room._id, willBeAbandonedAt);
};

export const updatePredictedVisitorAbandonment = () => {
if (settings.get('Livechat_auto_close_abandoned_rooms')) {
LivechatRooms.findLivechat({ open: true }).forEach((room) => setPredictedVisitorAbandonmentTime(room));
} else {
LivechatRooms.unsetPredictedVisitorAbandonment();
}
};
87 changes: 87 additions & 0 deletions ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { SyncedCron } from 'meteor/littledata:synced-cron';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';

import { settings } from '../../../../../app/settings/server';
import { LivechatRooms, LivechatDepartment, Users } from '../../../../../app/models/server';
import { Livechat } from '../../../../../app/livechat/server/lib/Livechat';

export class VisitorInactivityMonitor {
constructor() {
this._started = false;
this._name = 'Omnichannel Visitor Inactivity Monitor';
this.messageCache = new Map();
this.userToPerformAutomaticClosing;
}

start() {
Comment thread
MarcosSpessatto marked this conversation as resolved.
this._startMonitoring();
this._initializeMessageCache();
this.userToPerformAutomaticClosing = Users.findOneById('rocket.cat');
}

_startMonitoring() {
if (this.isRunning()) {
return;
}
const everyMinute = '* * * * *';
SyncedCron.add({
name: this._name,
schedule: (parser) => parser.cron(everyMinute),
job: () => {
this.handleAbandonedRooms();
},
});
this._started = true;
}

stop() {
if (!this.isRunning()) {
return;
}

SyncedCron.remove(this._name);

this._started = false;
}

isRunning() {
return this._started;
}

_initializeMessageCache() {
this.messageCache.clear();
this.messageCache.set('default', settings.get('Livechat_abandoned_rooms_closed_custom_message') || TAPi18n.__('Closed_automatically'));
}

_getDepartmentAbandonedCustomMessage(departmentId) {
if (this.messageCache.has('departmentId')) {
return this.messageCache.get('departmentId');
}
const department = LivechatDepartment.findOneById(departmentId);
if (!department) {
return;
}
this.messageCache.set(department._id, department.abandonedRoomsCloseCustomMessage);
return department.abandonedRoomsCloseCustomMessage;
}

closeRooms(room) {
let comment = this.messageCache.get('default');
if (room.departmentId) {
comment = this._getDepartmentAbandonedCustomMessage(room.departmentId) || comment;
}
Livechat.closeRoom({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The user who is closing the chats is required here. So, my suggestion is to use rocket.cat to as a user to perform this action.
Then you don't need to set system here:
https://github.com/RocketChat/Rocket.Chat/pull/17055/files#diff-3e725e94e4a647e422a10d903b96cf59R349

comment,
room,
user: this.userToPerformAutomaticClosing,
});
}

handleAbandonedRooms() {
if (!settings.get('Livechat_auto_close_abandoned_rooms')) {
return;
}
LivechatRooms.findAbandonedOpenRooms(new Date()).forEach((room) => this.closeRooms(room));
this._initializeMessageCache();
}
}
15 changes: 15 additions & 0 deletions ee/app/livechat-enterprise/server/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,20 @@ export const createSettings = () => {
enableQuery: { _id: 'Livechat_waiting_queue', value: true },
});

settings.add('Livechat_auto_close_abandoned_rooms', false, {
type: 'boolean',
group: 'Omnichannel',
section: 'Sessions',
i18nLabel: 'Enable_omnichannel_auto_close_abandoned_rooms',
});

settings.add('Livechat_abandoned_rooms_closed_custom_message', '', {
type: 'string',
group: 'Omnichannel',
section: 'Sessions',
i18nLabel: 'Livechat_abandoned_rooms_closed_custom_message',
enableQuery: { _id: 'Livechat_auto_close_abandoned_rooms', value: true },
});

Settings.addOptionValueById('Livechat_Routing_Method', { key: 'Load_Balancing', i18nLabel: 'Load_Balancing' });
};
15 changes: 14 additions & 1 deletion ee/app/livechat-enterprise/server/startup.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { Meteor } from 'meteor/meteor';

import { settings } from '../../../../app/settings';
import { checkWaitingQueue } from './lib/Helper';
import { checkWaitingQueue, updatePredictedVisitorAbandonment } from './lib/Helper';
import { VisitorInactivityMonitor } from './lib/VisitorInactivityMonitor';
import './lib/query.helper';

const visitorActivityMonitor = new VisitorInactivityMonitor();

Meteor.startup(function() {
settings.onload('Livechat_maximum_chats_per_agent', function(/* key, value */) {
checkWaitingQueue();
});
settings.onload('Livechat_auto_close_abandoned_rooms', function(_, value) {
updatePredictedVisitorAbandonment();
if (!value) {
return visitorActivityMonitor.stop();
}
visitorActivityMonitor.start();
});
settings.onload('Livechat_visitor_inactivity_timeout', function() {
updatePredictedVisitorAbandonment();
});
});
34 changes: 34 additions & 0 deletions ee/app/models/server/models/LivechatRooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,38 @@ overwriteClassOnLicense('livechat-enterprise', LivechatRooms, {
},
});


LivechatRooms.prototype.setPredictedVisitorAbandonment = function(roomId, willBeAbandonedAt) {
const query = {
_id: roomId,
};
const update = {
$set: {
'omnichannel.predictedVisitorAbandonmentAt': willBeAbandonedAt,
},
};

return this.update(query, update);
};

LivechatRooms.prototype.findAbandonedOpenRooms = function(date) {
return this.find({
'omnichannel.predictedVisitorAbandonmentAt': { $lte: date },
waitingResponse: { $exists: false },
closedAt: { $exists: false },
open: true,
});
};

LivechatRooms.prototype.unsetPredictedVisitorAbandonment = function() {
return this.update({
open: true,
t: 'l',
}, {
$unset: { 'omnichannel.predictedVisitorAbandonmentAt': 1 },
}, {
multi: true,
});
};

export default LivechatRooms;
Loading