[NEW][ENTERPRISE] Automatic transfer of unanswered conversations to another agent (#20090)
* [Omnichannel] Auto transfer chat based on inactivity * Updated Livechat.transfer() method to support a new param - ignoredUserId - This will prevent the transfer to the same agent - For Manual selection method, place the chat back to the queue, if the agents doesn't respond back within the set duration * Apply suggestions from code review Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com> * Apply suggestion from code review * Fix merge conflict * Apply suggestions from code review * Fix PR review. * cancel previous jobs b4 scheduling new one + minor improvements * Use a dedicated variable to read setting value. * [optimize] prevent cancelling job after each message sent * Improve codebase. * Remove unnecessary import. * Add PT-BR translations. * Fix class methods. * Improve class code. * Added final improvements to the codebase. * remove unnused import files. * Move hardcoded variables to const. Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com>pull/20196/head^2
parent
768e709234
commit
f7caaf207c
@ -0,0 +1,84 @@ |
||||
import { AutoTransferChatScheduler } from '../lib/AutoTransferChatScheduler'; |
||||
import { callbacks } from '../../../../../app/callbacks/server'; |
||||
import { settings } from '../../../../../app/settings/server'; |
||||
import { LivechatRooms } from '../../../../../app/models/server'; |
||||
|
||||
let autoTransferTimeout = 0; |
||||
|
||||
const handleAfterTakeInquiryCallback = async (inquiry: any = {}): Promise<any> => { |
||||
const { rid } = inquiry; |
||||
if (!rid || !rid.trim()) { |
||||
return; |
||||
} |
||||
|
||||
if (!autoTransferTimeout || autoTransferTimeout <= 0) { |
||||
return inquiry; |
||||
} |
||||
|
||||
const room = LivechatRooms.findOneById(rid, { autoTransferredAt: 1, autoTransferOngoing: 1 }); |
||||
if (!room || room.autoTransferredAt || room.autoTransferOngoing) { |
||||
return inquiry; |
||||
} |
||||
|
||||
await AutoTransferChatScheduler.scheduleRoom(rid, autoTransferTimeout as number); |
||||
|
||||
return inquiry; |
||||
}; |
||||
|
||||
const handleAfterSaveMessage = async (message: any = {}, room: any = {}): Promise<any> => { |
||||
const { _id: rid, t, autoTransferredAt, autoTransferOngoing } = room; |
||||
const { token } = message; |
||||
|
||||
if (!autoTransferTimeout || autoTransferTimeout <= 0) { |
||||
return message; |
||||
} |
||||
|
||||
if (!rid || !message || rid === '' || t !== 'l' || token) { |
||||
return message; |
||||
} |
||||
|
||||
if (autoTransferredAt) { |
||||
return message; |
||||
} |
||||
|
||||
if (!autoTransferOngoing) { |
||||
return message; |
||||
} |
||||
|
||||
await AutoTransferChatScheduler.unscheduleRoom(rid); |
||||
return message; |
||||
}; |
||||
|
||||
|
||||
const handleAfterCloseRoom = async (room: any = {}): Promise<any> => { |
||||
const { _id: rid, autoTransferredAt, autoTransferOngoing } = room; |
||||
|
||||
if (!autoTransferTimeout || autoTransferTimeout <= 0) { |
||||
return room; |
||||
} |
||||
|
||||
if (autoTransferredAt) { |
||||
return room; |
||||
} |
||||
|
||||
if (!autoTransferOngoing) { |
||||
return room; |
||||
} |
||||
|
||||
await AutoTransferChatScheduler.unscheduleRoom(rid); |
||||
return room; |
||||
}; |
||||
|
||||
settings.get('Livechat_auto_transfer_chat_timeout', function(_, value) { |
||||
autoTransferTimeout = value as number; |
||||
if (!autoTransferTimeout || autoTransferTimeout === 0) { |
||||
callbacks.remove('livechat.afterTakeInquiry', 'livechat-auto-transfer-job-inquiry'); |
||||
callbacks.remove('afterSaveMessage', 'livechat-cancel-auto-transfer-job-after-message'); |
||||
callbacks.remove('livechat.closeRoom', 'livechat-cancel-auto-transfer-on-close-room'); |
||||
return; |
||||
} |
||||
|
||||
callbacks.add('livechat.afterTakeInquiry', handleAfterTakeInquiryCallback, callbacks.priority.MEDIUM, 'livechat-auto-transfer-job-inquiry'); |
||||
callbacks.add('afterSaveMessage', handleAfterSaveMessage, callbacks.priority.HIGH, 'livechat-cancel-auto-transfer-job-after-message'); |
||||
callbacks.add('livechat.closeRoom', handleAfterCloseRoom, callbacks.priority.HIGH, 'livechat-cancel-auto-transfer-on-close-room'); |
||||
}); |
||||
@ -0,0 +1,89 @@ |
||||
import Agenda from 'agenda'; |
||||
import { MongoInternals } from 'meteor/mongo'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { LivechatRooms, Users } from '../../../../../app/models/server'; |
||||
import { Livechat } from '../../../../../app/livechat/server'; |
||||
import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; |
||||
import { forwardRoomToAgent } from '../../../../../app/livechat/server/lib/Helper'; |
||||
|
||||
const schedulerUser = Users.findOneById('rocket.cat'); |
||||
const SCHEDULER_NAME = 'omnichannel_scheduler'; |
||||
|
||||
class AutoTransferChatSchedulerClass { |
||||
scheduler: Agenda; |
||||
|
||||
running: boolean; |
||||
|
||||
user: {}; |
||||
|
||||
public init(): void { |
||||
if (this.running) { |
||||
return; |
||||
} |
||||
|
||||
this.scheduler = new Agenda({ |
||||
mongo: (MongoInternals.defaultRemoteCollectionDriver().mongo as any).client.db(), |
||||
db: { collection: SCHEDULER_NAME }, |
||||
defaultConcurrency: 1, |
||||
}); |
||||
|
||||
this.scheduler.start(); |
||||
this.running = true; |
||||
} |
||||
|
||||
public async scheduleRoom(roomId: string, timeout: number): Promise<void> { |
||||
await this.unscheduleRoom(roomId); |
||||
|
||||
const jobName = `${ SCHEDULER_NAME }-${ roomId }`; |
||||
const when = new Date(); |
||||
when.setSeconds(when.getSeconds() + timeout); |
||||
|
||||
this.scheduler.define(jobName, this.executeJob.bind(this)); |
||||
await this.scheduler.schedule(when, jobName, { roomId }); |
||||
await LivechatRooms.setAutoTransferOngoingById(roomId); |
||||
} |
||||
|
||||
public async unscheduleRoom(roomId: string): Promise<void> { |
||||
const jobName = `${ SCHEDULER_NAME }-${ roomId }`; |
||||
|
||||
await LivechatRooms.unsetAutoTransferOngoingById(roomId); |
||||
await this.scheduler.cancel({ name: jobName }); |
||||
} |
||||
|
||||
private async transferRoom(roomId: string): Promise<boolean> { |
||||
const room = LivechatRooms.findOneById(roomId, { _id: 1, v: 1, servedBy: 1, open: 1, departmentId: 1 }); |
||||
if (!room?.open || !room?.servedBy?._id) { |
||||
return false; |
||||
} |
||||
|
||||
const { departmentId, servedBy: { _id: ignoreAgentId } } = room; |
||||
|
||||
if (!RoutingManager.getConfig().autoAssignAgent) { |
||||
return Livechat.returnRoomAsInquiry(room._id, departmentId); |
||||
} |
||||
|
||||
const agent = await RoutingManager.getNextAgent(departmentId, ignoreAgentId); |
||||
if (agent) { |
||||
return forwardRoomToAgent(room, { userId: agent.agentId, transferredBy: schedulerUser, transferredTo: agent }); |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
private async executeJob({ attrs: { data } }: any = {}): Promise<void> { |
||||
const { roomId } = data; |
||||
|
||||
if (await this.transferRoom(roomId)) { |
||||
LivechatRooms.setAutoTransferredAtById(roomId); |
||||
} |
||||
|
||||
await this.unscheduleRoom(roomId); |
||||
} |
||||
} |
||||
|
||||
export const AutoTransferChatScheduler = new AutoTransferChatSchedulerClass(); |
||||
|
||||
Meteor.startup(() => { |
||||
AutoTransferChatScheduler.init(); |
||||
}); |
||||
Loading…
Reference in new issue