Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
10 changes: 8 additions & 2 deletions SalesforceLiveAgentApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,14 @@ export class SalesforceLiveAgentApp
await postMessageClassInitiate.exec();
}

public async executeOnRoomUserTyping(data: IRoomUserTypingContext, read: IRead, http: IHttp, persistence: IPersistence): Promise<void> {
const onUserTypingHandler = new OnUserTypingHandler(this, data, read, http, persistence);
public async executeOnRoomUserTyping(
data: IRoomUserTypingContext,
read: IRead,
http: IHttp,
persistence: IPersistence,
modify: IModify,
): Promise<void> {
const onUserTypingHandler = new OnUserTypingHandler(this, data, read, http, persistence, modify);
await onUserTypingHandler.exec();
}

Expand Down
177 changes: 105 additions & 72 deletions helperFunctions/TimeoutHelper.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,115 @@
import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors';
import { IApp } from '@rocket.chat/apps-engine/definition/IApp';
import { IMessage } from '@rocket.chat/apps-engine/definition/messages';
import { RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata';
import { IOnetimeSchedule } from '@rocket.chat/apps-engine/definition/scheduler';
import { AppSettingId } from '../enum/AppSettingId';
import { getRoomAssoc, retrievePersistentData, updatePersistentData } from '../helperFunctions/PersistenceHelpers';
import { getAppSettingValue } from '../lib/Settings';

async function updateSneakPeekField(
salesforceBotUsername: string,
messageId: string,
sneakPeekEnabled: boolean | null,
read: IRead,
modify: IModify,
) {
const user = await read.getUserReader().getByUsername(salesforceBotUsername);
const msgExtender = await modify.getExtender().extendMessage(messageId, user);
msgExtender.addCustomField('sneakPeekEnabled', sneakPeekEnabled);
return modify.getExtender().finish(msgExtender);
}

async function modifyWidgetTimer(
salesforceBotUsername: string,
message: IMessage,
chasitorIdleTimeout: any,
idleTimeoutAction: string,
read: IRead,
modify: IModify,
) {
const timeoutWarningMessage: string = await getAppSettingValue(read, AppSettingId.CUSTOMER_TIMEOUT_WARNING_MESSAGE);
const warningTime = chasitorIdleTimeout.warningTime;
const timeoutTime = chasitorIdleTimeout.timeout;

const user = await read.getUserReader().getByUsername(salesforceBotUsername);
const msgExtender = await modify.getExtender().extendMessage(message.id as string, user);
msgExtender.addCustomField('idleTimeoutConfig', {
idleTimeoutAction,
idleTimeoutWarningTime: warningTime,
idleTimeoutTimeoutTime: timeoutTime,
idleTimeoutMessage: timeoutWarningMessage,
});
}

async function clearAppTimeout(roomId: string, read: IRead, modify: IModify, persistence: IPersistence, app: IApp, assoc) {
await updatePersistentData(read, persistence, assoc, { isIdleSessionTimerScheduled: false, idleSessionTimerId: '' });
await modify.getScheduler().cancelJobByDataQuery({ rid: roomId, taskType: 'sessionTimeout' });
}

async function scheduleAppTimeout(
roomId: string,
chasitorIdleTimeout: any,
read: IRead,
modify: IModify,
persistence: IPersistence,
assoc: RocketChatAssociationRecord,
) {
const { isIdleSessionTimerScheduled, idleSessionTimerId } = await retrievePersistentData(read, assoc);

if (isIdleSessionTimerScheduled === true) {
if (idleSessionTimerId) {
await modify.getScheduler().cancelJob(idleSessionTimerId);
}
}

const task: IOnetimeSchedule = {
id: 'idle-session-timeout',
when: new Date(new Date().getTime() + chasitorIdleTimeout.timeout * 1000),
data: { rid: roomId, taskType: 'sessionTimeout' },
};
const jobId = await modify.getScheduler().scheduleOnce(task);
await updatePersistentData(read, persistence, assoc, { isIdleSessionTimerScheduled: true, idleSessionTimerId: jobId });
}

/**
* Clears existing app timeout timer, and instantiates a new app timeout timer
*/
export async function resetAppTimeout(
roomId: string,
chasitorIdleTimeout: any,
read: IRead,
modify: IModify,
persistence: IPersistence,
app: IApp,
assoc: RocketChatAssociationRecord,
) {
const sessionTimeoutHandler: string = await getAppSettingValue(read, AppSettingId.TIMEOUT_HANDLER);
if (sessionTimeoutHandler === 'app') {
await clearAppTimeout(roomId, read, modify, persistence, app, assoc);
await scheduleAppTimeout(roomId, chasitorIdleTimeout, read, modify, persistence, assoc);
}
}

/**
* Sets the amount of time that a customer has to respond to an agent message before a warning appears and a timer begins a countdown.
*
* - The warning disappears (and the timer stops) each time the customer sends a message.
* - The warning disappears (and the timer resets to 0) each time the agent sends message.
* - The timer stops when the customer sends a message and starts again from 0 on the next agent's message.
*
* The warning value must be shorter than the time-out value (we recommend at least 30 seconds).
*/
export const handleTimeout = async (app: IApp, message: IMessage, read: IRead, http: IHttp, persistence: IPersistence, modify: IModify) => {
if (message.room.type !== 'l' || (message.customFields && message.customFields.idleTimeoutConfig)) {
if (message.customFields?.idleTimeoutConfig) {
return;
}

const salesforceBotUsername: string = await getAppSettingValue(read, AppSettingId.SALESFORCE_BOT_USERNAME);
const assoc = getRoomAssoc(message.room.id);
const { chasitorIdleTimeout, sneakPeekEnabled } = await retrievePersistentData(read, assoc);

if (chasitorIdleTimeout && chasitorIdleTimeout.isEnabled) {
/**
* Sets the amount of time that a customer has to respond to an agent message before a warning appears and a timer begins a countdown.
* The warning disappears (and the timer stops) each time the customer sends a message.
* The warning disappears (and the timer resets to 0) each time the agent sends message.
* The warning value must be shorter than the time-out value (we recommend at least 30 seconds).
*/
const warningTime = chasitorIdleTimeout.warningTime;

/**
* Sets the amount of time that a customer has to respond to an agent message before the session ends.
* The timer stops when the customer sends a message and starts again from 0 on the next agent's message.
*/
const timeoutTime = chasitorIdleTimeout.timeout;

if (chasitorIdleTimeout?.isEnabled) {
// ------ When agent sends message -----
// Send new timeout msg and reset previous timeout

Expand All @@ -39,7 +119,6 @@ export const handleTimeout = async (app: IApp, message: IMessage, read: IRead, h
// On Timeout : Close chat
// On Warning : Show Countdown Popup in Livechat Widget

const timeoutWarningMessage: string = await getAppSettingValue(read, AppSettingId.CUSTOMER_TIMEOUT_WARNING_MESSAGE);
const sessionTimeoutHandler: string = await getAppSettingValue(read, AppSettingId.TIMEOUT_HANDLER);

if (message.sender.username === salesforceBotUsername) {
Expand All @@ -49,75 +128,29 @@ export const handleTimeout = async (app: IApp, message: IMessage, read: IRead, h
}

if (sessionTimeoutHandler === 'app') {
await scheduleTimeOut(message, read, modify, persistence, timeoutTime, app, assoc);
await scheduleAppTimeout(message.room.id, chasitorIdleTimeout, read, modify, persistence, assoc);
}

const user = await read.getUserReader().getByUsername(salesforceBotUsername);
const msgExtender = modify.getExtender().extendMessage(message.id, user);
(await msgExtender).addCustomField('idleTimeoutConfig', {
idleTimeoutAction: 'start',
idleTimeoutWarningTime: warningTime,
idleTimeoutTimeoutTime: timeoutTime,
idleTimeoutMessage: timeoutWarningMessage,
});
(await msgExtender).addCustomField('sneakPeekEnabled', sneakPeekEnabled);
modify.getExtender().finish(await msgExtender);
await modifyWidgetTimer(salesforceBotUsername, message, chasitorIdleTimeout, 'start', read, modify);
await updateSneakPeekField(salesforceBotUsername, message.room.id, sneakPeekEnabled, read, modify);
} else {
// Guest sent message

if (!message.id) {
return;
}

if (sessionTimeoutHandler === 'app') {
await updatePersistentData(read, persistence, assoc, { isIdleSessionTimerScheduled: false, idleSessionTimerId: '' });
await modify.getScheduler().cancelJobByDataQuery({ rid: message.room.id, taskType: 'sessionTimeout' });
await clearAppTimeout(message.room.id, read, modify, persistence, app, assoc);
}
const user = await read.getUserReader().getByUsername(salesforceBotUsername);
const msgExtender = modify.getExtender().extendMessage(message.id, user);
(await msgExtender).addCustomField('idleTimeoutConfig', {
idleTimeoutAction: 'stop',
idleTimeoutWarningTime: warningTime,
idleTimeoutTimeoutTime: timeoutTime,
idleTimeoutMessage: timeoutWarningMessage,
});
(await msgExtender).addCustomField('sneakPeekEnabled', sneakPeekEnabled);
modify.getExtender().finish(await msgExtender);

await modifyWidgetTimer(salesforceBotUsername, message, chasitorIdleTimeout, 'stop', read, modify);
await updateSneakPeekField(salesforceBotUsername, message.room.id, sneakPeekEnabled, read, modify);
}
} else {
if (!message.id) {
return;
}
const user = await read.getUserReader().getByUsername(salesforceBotUsername);
const msgExtender = modify.getExtender().extendMessage(message.id, user);
(await msgExtender).addCustomField('sneakPeekEnabled', sneakPeekEnabled);
modify.getExtender().finish(await msgExtender);
}
};

async function scheduleTimeOut(
message: IMessage,
read: IRead,
modify: IModify,
persistence: IPersistence,
idleTimeoutTimeoutTime: number,
app: IApp,
assoc,
) {
const rid = message.room.id;
const { isIdleSessionTimerScheduled, idleSessionTimerId } = await retrievePersistentData(read, assoc);

if (isIdleSessionTimerScheduled === true) {
if (idleSessionTimerId) {
await modify.getScheduler().cancelJob(idleSessionTimerId);
}
await updateSneakPeekField(salesforceBotUsername, message.id, sneakPeekEnabled, read, modify);
}

const task: IOnetimeSchedule = {
id: 'idle-session-timeout',
when: new Date(new Date().getTime() + idleTimeoutTimeoutTime * 1000),
data: { rid, taskType: 'sessionTimeout' },
};
const jobId = await modify.getScheduler().scheduleOnce(task);
await updatePersistentData(read, persistence, assoc, { isIdleSessionTimerScheduled: true, idleSessionTimerId: jobId });
}
};
49 changes: 34 additions & 15 deletions lib/OnUserTypingHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IHttp, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors';
import { IHttp, IPersistence, IRead, IModify } from '@rocket.chat/apps-engine/definition/accessors';
import { IApp } from '@rocket.chat/apps-engine/definition/IApp';
import { ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat';
import { IRoomUserTypingContext } from '@rocket.chat/apps-engine/definition/rooms';
Expand All @@ -7,6 +7,13 @@ import { ErrorLogs } from '../enum/ErrorLogs';
import { getRoomAssoc, retrievePersistentData } from '../helperFunctions/PersistenceHelpers';
import { chasitorSneakPeak, chasitorTyping } from '../helperFunctions/SalesforceAPIHelpers';
import { getAppSettingValue } from '../lib/Settings';
import { resetAppTimeout } from '../helperFunctions/TimeoutHelper';

let allowTimeoutReset = true;

let userStoppedTypingTimeout: NodeJS.Timeout | null = null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@csmith14 Were you able to test these changes?

As far as I know, setTimeout does not work in the RC Apps.

And also using allowTimeoutReset, and userStoppedTypingTimeout as a condition won't work here because in the real world this function will be called for multiple chat/user. So callback for one chat will override the flag for other chats.


const TIMER_RESET_DELAY_SECONDS = 10;

export class OnUserTypingHandler {
constructor(
Expand All @@ -15,6 +22,7 @@ export class OnUserTypingHandler {
private read: IRead,
private http: IHttp,
private persistence: IPersistence,
private modify: IModify,
) {}

public async exec() {
Expand Down Expand Up @@ -43,27 +51,38 @@ export class OnUserTypingHandler {
return;
}
const assoc = getRoomAssoc(this.data.roomId);
const { persistentAffinity, persistentKey, sneakPeekEnabled } = await retrievePersistentData(this.read, assoc);
const { persistentAffinity, persistentKey, sneakPeekEnabled, chasitorIdleTimeout } = await retrievePersistentData(this.read, assoc);

if (persistentAffinity !== null && persistentKey !== null) {
// reset the app timer when user typing handler is called for the first time in >=10 seconds
if (allowTimeoutReset) {
await resetAppTimeout(room.id, chasitorIdleTimeout, this.read, this.modify, this.persistence, this.app, assoc);
allowTimeoutReset = false;
}

// If there is an existing timer (for >=10s of NO typing handler calls), clear it (in order to reset it)
if (userStoppedTypingTimeout !== null) {
clearTimeout(userStoppedTypingTimeout);
}

// schedule timer to allow typing handler to reset app timer after >=10 seconds of user not typing
userStoppedTypingTimeout = setTimeout(async () => {
allowTimeoutReset = true; // allow app timer reset to be called on next user typing handler call
userStoppedTypingTimeout = null;
}, TIMER_RESET_DELAY_SECONDS * 1000);

if (sneakPeekEnabled) {
if (this.data.data.text || this.data.data.text === '') {
await chasitorSneakPeak(this.http, salesforceChatApiEndpoint, persistentAffinity, persistentKey, this.data.data.text)
.then(async () => {
// ChasitorSneakPeak API Success
})
.catch((error) => {
await chasitorSneakPeak(this.http, salesforceChatApiEndpoint, persistentAffinity, persistentKey, this.data.data.text).catch(
(error) => {
console.error(ErrorLogs.CHASITOR_SNEAKPEEK_API_CALL_FAIL, error);
});
},
);
}
} else {
await chasitorTyping(this.http, salesforceChatApiEndpoint, persistentAffinity, persistentKey, this.data.typing)
.then(async () => {
// ChasitorTyping/ChasitorNotTyping API Success
})
.catch((error) => {
console.error(ErrorLogs.CHASITOR_TYPING_API_CALL_FAIL, error);
});
await chasitorTyping(this.http, salesforceChatApiEndpoint, persistentAffinity, persistentKey, this.data.typing).catch((error) => {
console.error(ErrorLogs.CHASITOR_TYPING_API_CALL_FAIL, error);
});
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/PostMessageClassInitiateHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class PostMessageClassInitiate {

handleTimeout(this.app, this.message, this.read, this.http, this.persistence, this.modify);

if (this.message && this.message.id) {
if (this.message?.id) {
const { salesforceAgentName } = await retrievePersistentData(this.read, assoc);
const user = await this.read.getUserReader().getByUsername(salesforceBotUsername);
const msgExtender = this.modify.getExtender().extendMessage(this.message.id, user);
Expand Down