scv-connector-base
Version:
Salesforce Service Cloud Connector Base
951 lines (928 loc) • 71.2 kB
JavaScript
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
/* eslint-disable no-unused-vars */
import constants from './constants.js';
import { CONNECTOR_CONFIG_EXPOSED_FIELDS, CONNECTOR_CONFIG_EXPOSED_FIELDS_STARTSWITH, CONNECTOR_CONFIG_EXCEPTION_FIELDS } from './constants.js';
import {
Validator,
GenericResult,
InitResult,
CallResult,
HangupResult,
HoldToggleResult,
ContactsResult,
PhoneContactsResult,
MuteToggleResult,
ParticipantResult,
RecordingToggleResult,
AgentConfigResult,
ActiveCallsResult,
SignedRecordingUrlResult,
LogoutResult,
VendorConnector,
Contact,
AudioStats,
SuperviseCallResult,
SupervisorHangupResult,
AgentStatusInfo,
SupervisedCallInfo,
SharedCapabilitiesResult,
VoiceCapabilitiesResult,
AgentVendorStatusInfo,
StateChangeResult,
CustomError,
DialOptions,
ShowStorageAccessResult,
AudioDevicesResult,
ACWInfo,
SetAgentConfigResult,
SetAgentStateResult
} from './types';
import { enableMos, getMOS, initAudioStats, updateAudioStats } from './mosUtil';
import { log, getLogs } from './logger';
let channelPort;
let vendorConnector;
let agentAvailable;
let isSupervisorConnected;
/**
* Gets the error type from the error object
* @param {object} e Error object representing the error
*/
function getErrorType(e) {
return e && e.type ? e.type : e;
}
/**
* Sanitizes the object by removing any PII data
* @param {object} payload
*/
function sanitizePayload(payload) {
if (payload) {
if (typeof (payload) === 'function') {
// remove functions from the payload, because they cannot be copied by the postMessage function
return;
} else if (typeof (payload) === 'object') {
const isArray = Array.isArray(payload);
const sanitizedPayload = isArray ? [] : {};
if (isArray) {
payload.forEach(element => {
sanitizedPayload.push(sanitizePayload(element));
});
} else {
for (const property in payload) {
if (property !== 'phoneNumber' &&
property !== 'number' &&
property !== 'name' &&
property !== 'callAttributes' &&
property !== '/reqHvcc/reqTelephonyIntegrationCertificate') {
sanitizedPayload[property] = sanitizePayload(payload[property]);
}
}
}
return sanitizedPayload;
}
}
return payload;
}
/**
* Gets the error message from the error object
* @param {object} e Error object representing the error
*/
function getErrorMessage(e) {
return e && e.message ? e.message : e;
}
/**
* Dispatch a telephony event log to Salesforce
* @param {String} eventType event type, i.e. constants.VOICE_EVENT_TYPE.CALL_STARTED
* @param {Object} payload event payload
* @param {Boolean} isError error scenario
*/
function dispatchEventLog(eventType, payload, isError) {
const sanitizedPayload = sanitizePayload(payload);
const logLevel = isError ? constants.LOG_LEVEL.ERROR : constants.LOG_LEVEL.INFO;
log({eventType, payload}, logLevel, constants.LOG_SOURCE.SYSTEM);
channelPort.postMessage({
type: constants.SHARED_MESSAGE_TYPE.LOG,
payload: { eventType, payload: sanitizedPayload, isError }
});
}
/**
* Dispatch a telephony event to Salesforce
* @param {String} eventType event type, i.e. constants.VOICE_EVENT_TYPE.CALL_STARTED
* @param {Object} payload event payload
* @param {Boolean} registerLog optional argument to not register the event
*/
function dispatchEvent(eventType, payload, registerLog = true) {
channelPort.postMessage({
type: constants.SHARED_MESSAGE_TYPE.TELEPHONY_EVENT_DISPATCHED,
payload: { telephonyEventType: eventType, telephonyEventPayload: payload }
});
if (registerLog) {
dispatchEventLog(eventType, payload, false);
}
}
/**
* Dispatch a telephony integration error to Salesforce
* @param {constants.VOICE_ERROR_TYPE} errorType Error Type, ex: constants.VOICE_ERROR_TYPE.MICROPHONE_NOT_SHARED
* @param {object} error Error object representing the error
* @param {string} eventType The event that caused this error, ex: constants.VOICE_MESSAGE_TYPE.ACCEPT_CALL
*/
function dispatchError(errorType, error, eventType) {
// eslint-disable-next-line no-console
console.error(`SCV dispatched error ${errorType} for eventType ${eventType}`, error);
dispatchEvent(constants.SHARED_EVENT_TYPE.ERROR, { message: errorType }, false);
dispatchEventLog(eventType, { errorType, error }, true);
}
/**
* Dispatch a telephony integration error to Salesforce
* @param {CustomError} error Error object representing the custom error
* @param {string} eventType The event that caused this error, ex: constants.SHARED_MESSAGE_TYPE.ACCEPT_CALL
*/
function dispatchCustomError(error, eventType) {
// eslint-disable-next-line no-console
const payload = {
customError: {
labelName: error.labelName,
namespace: error.namespace,
message: error.message
}
};
console.error(`SCV dispatched custom error for eventType ${eventType}`, payload);
dispatchEvent(constants.SHARED_EVENT_TYPE.ERROR, payload, false);
dispatchEventLog(eventType, { errorType: constants.SHARED_ERROR_TYPE.CUSTOM_ERROR, error }, true);
}
function dispatchInfo(eventType, payload) {
// eslint-disable-next-line no-console
console.info(`SCV info message dispatched for eventType ${eventType} with payload ${JSON.stringify(payload)}`);
dispatchEvent(constants.SHARED_EVENT_TYPE.INFO, { message: eventType }, false);
dispatchEventLog(eventType, payload, false);
}
/**
* Notify Salesforce that the connector is ready
*/
async function setConnectorReady() {
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const agentConfigResult = await telephonyConnector.getAgentConfig();
const sharedCapabilitiesResult = await vendorConnector.getSharedCapabilities();
const voiceCapabilitiesResult = await telephonyConnector.getVoiceCapabilities();
Validator.validateClassObject(agentConfigResult, AgentConfigResult);
Validator.validateClassObject(voiceCapabilitiesResult, VoiceCapabilitiesResult);
if (voiceCapabilitiesResult.supportsMos) {
enableMos();
}
const activeCallsResult = await telephonyConnector.getActiveCalls();
Validator.validateClassObject(activeCallsResult, ActiveCallsResult);
const activeCalls = activeCallsResult.activeCalls;
const type = constants.SHARED_MESSAGE_TYPE.CONNECTOR_READY;
const payload = {
agentConfig: {
[constants.AGENT_CONFIG_TYPE.PHONES] : agentConfigResult.phones,
[constants.AGENT_CONFIG_TYPE.SELECTED_PHONE] : agentConfigResult.selectedPhone
},
capabilities: {
[constants.SHARED_CAPABILITIES_TYPE.DEBUG_ENABLED] : sharedCapabilitiesResult.debugEnabled,
[constants.SHARED_CAPABILITIES_TYPE.CONTACT_SEARCH] : sharedCapabilitiesResult.hasContactSearch,
[constants.SHARED_CAPABILITIES_TYPE.VENDOR_PROVIDED_AVAILABILITY] : sharedCapabilitiesResult.hasAgentAvailability,
[constants.SHARED_CAPABILITIES_TYPE.VENDOR_PROVIDED_QUEUE_WAIT_TIME] : sharedCapabilitiesResult.hasQueueWaitTime,
[constants.SHARED_CAPABILITIES_TYPE.TRANSFER_TO_OMNI_FLOW] : sharedCapabilitiesResult.hasTransferToOmniFlow,
[constants.SHARED_CAPABILITIES_TYPE.PENDING_STATUS_CHANGE] : sharedCapabilitiesResult.hasPendingStatusChange,
[constants.SHARED_CAPABILITIES_TYPE.SFDC_PENDING_STATE]: sharedCapabilitiesResult.hasSFDCPendingState,
[constants.SHARED_CAPABILITIES_TYPE.AUTO_ACCEPT_ENABLED]: sharedCapabilitiesResult.hasAutoAcceptEnabled,
[constants.VOICE_CAPABILITIES_TYPE.MUTE] : voiceCapabilitiesResult.hasMute,
[constants.VOICE_CAPABILITIES_TYPE.RECORD] : voiceCapabilitiesResult.hasRecord,
[constants.VOICE_CAPABILITIES_TYPE.MERGE] : voiceCapabilitiesResult.hasMerge,
[constants.VOICE_CAPABILITIES_TYPE.SWAP] : voiceCapabilitiesResult.hasSwap,
[constants.VOICE_CAPABILITIES_TYPE.BLIND_TRANSFER] : voiceCapabilitiesResult.hasBlindTransfer,
[constants.VOICE_CAPABILITIES_TYPE.SIGNED_RECORDING_URL] : voiceCapabilitiesResult.hasSignedRecordingUrl,
[constants.VOICE_CAPABILITIES_TYPE.SUPERVISOR_LISTEN_IN] : voiceCapabilitiesResult.hasSupervisorListenIn,
[constants.VOICE_CAPABILITIES_TYPE.SUPERVISOR_BARGE_IN] : voiceCapabilitiesResult.hasSupervisorBargeIn,
[constants.VOICE_CAPABILITIES_TYPE.MOS] : voiceCapabilitiesResult.supportsMos,
[constants.VOICE_CAPABILITIES_TYPE.PHONEBOOK] : voiceCapabilitiesResult.hasPhoneBook,
[constants.VOICE_CAPABILITIES_TYPE.HAS_GET_EXTERNAL_SPEAKER] : voiceCapabilitiesResult.hasGetExternalSpeakerDeviceSetting,
[constants.VOICE_CAPABILITIES_TYPE.HAS_SET_EXTERNAL_SPEAKER] : voiceCapabilitiesResult.hasSetExternalSpeakerDeviceSetting,
[constants.VOICE_CAPABILITIES_TYPE.HAS_GET_EXTERNAL_MICROPHONE] : voiceCapabilitiesResult.hasGetExternalMicrophoneDeviceSetting,
[constants.VOICE_CAPABILITIES_TYPE.HAS_SET_EXTERNAL_MICROPHONE] : voiceCapabilitiesResult.hasSetExternalMicrophoneDeviceSetting,
[constants.VOICE_CAPABILITIES_TYPE.CAN_CONSULT]: voiceCapabilitiesResult.canConsult,
[constants.VOICE_CAPABILITIES_TYPE.DIAL_PAD]: voiceCapabilitiesResult.isDialPadDisabled,
[constants.VOICE_CAPABILITIES_TYPE.HAS_HID_SUPPORT]: voiceCapabilitiesResult.isHidSupported,
[constants.VOICE_CAPABILITIES_TYPE.PHONEBOOK_DISABLE]: voiceCapabilitiesResult.isPhoneBookDisabled
},
callInProgress: activeCalls.length > 0 ? activeCalls[0] : null
}
channelPort.postMessage({
type,
payload
});
dispatchEventLog(type, payload, false);
} catch (e) {
// Post CONNECTOR_READY even if getAgentConfig is not implemented
channelPort.postMessage({
type: constants.SHARED_MESSAGE_TYPE.CONNECTOR_READY,
payload: {}
});
dispatchEventLog(constants.SHARED_MESSAGE_TYPE.CONNECTOR_READY, {}, false);
}
}
//TODO: 230 we should convert call object to PhoneCall object
async function channelMessageHandler(message) {
const eventType = message.data.type;
if (eventType !== constants.SHARED_MESSAGE_TYPE.LOG) {
dispatchEventLog(eventType, message.data, false);
}
switch (eventType) {
case constants.VOICE_MESSAGE_TYPE.ACCEPT_CALL:
try {
if (message.data.call && message.data.call.callType &&
(message.data.call.callType.toLowerCase() === constants.CALL_TYPE.OUTBOUND.toLowerCase() ||
message.data.call.callType.toLowerCase() === constants.CALL_TYPE.DIALED_CALLBACK.toLowerCase())) {
return;
}
initAudioStats();
const telephonyConnector = await vendorConnector.getTelephonyConnector();
if (isSupervisorConnected) {
const hangupPayload = await telephonyConnector.supervisorDisconnect();
Validator.validateClassObject(hangupPayload, SupervisorHangupResult);
isSupervisorConnected = false;
dispatchEvent(constants.VOICE_EVENT_TYPE.SUPERVISOR_HANGUP, hangupPayload.calls);
}
let payload = await telephonyConnector.acceptCall(message.data.call);
Validator.validateClassObject(payload, CallResult);
const { call } = payload;
dispatchEvent(call.callType.toLowerCase() === constants.CALL_TYPE.CALLBACK.toLowerCase() ?
constants.VOICE_EVENT_TYPE.CALL_STARTED : constants.VOICE_EVENT_TYPE.CALL_CONNECTED, call);
} catch (e) {
isSupervisorConnected = false;
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.ACCEPT_CALL);
} else {
dispatchInfo(constants.INFO_TYPE.CAN_NOT_ACCEPT_THE_CALL, {messagetype: constants.VOICE_MESSAGE_TYPE.ACCEPT_CALL, additionalInfo: e});
}
}
break;
case constants.VOICE_MESSAGE_TYPE.DECLINE_CALL:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const payload = await telephonyConnector.declineCall(message.data.call);
Validator.validateClassObject(payload, CallResult);
const { call } = payload;
dispatchEvent(constants.VOICE_EVENT_TYPE.HANGUP, call);
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.DECLINE_CALL);
} else {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_DECLINE_THE_CALL, e, constants.VOICE_MESSAGE_TYPE.DECLINE_CALL);
}
}
break;
case constants.VOICE_MESSAGE_TYPE.END_CALL:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const payload = await telephonyConnector.endCall(message.data.call, message.data.agentStatus);
Validator.validateClassObject(payload, HangupResult);
const activeCallsResult = await telephonyConnector.getActiveCalls();
Validator.validateClassObject(activeCallsResult, ActiveCallsResult);
const activeCalls = activeCallsResult.activeCalls;
const { calls } = payload;
// after end calls from vendor side, if no more active calls, fire HANGUP, otherwise, fire PARTICIPANT_REMOVED
if (activeCalls.length === 0) {
dispatchEvent(constants.VOICE_EVENT_TYPE.HANGUP, calls);
} else {
dispatchEvent(constants.VOICE_EVENT_TYPE.PARTICIPANT_REMOVED, calls.length > 0 && calls[0]);
}
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.END_CALL);
} else {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_END_THE_CALL, e, constants.VOICE_MESSAGE_TYPE.END_CALL);
}
}
break;
case constants.VOICE_MESSAGE_TYPE.MUTE:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const payload = await telephonyConnector.mute(message.data.call);
publishEvent({eventType: constants.VOICE_EVENT_TYPE.MUTE_TOGGLE, payload});
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.MUTE);
} else {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_MUTE_CALL, e, constants.VOICE_MESSAGE_TYPE.MUTE);
}
}
break;
case constants.VOICE_MESSAGE_TYPE.UNMUTE:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const payload = await telephonyConnector.unmute(message.data.call);
publishEvent({eventType: constants.VOICE_EVENT_TYPE.MUTE_TOGGLE, payload});
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.UNMUTE);
} else {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_UNMUTE_CALL, e, constants.VOICE_MESSAGE_TYPE.UNMUTE);
}
}
break;
case constants.VOICE_MESSAGE_TYPE.HOLD:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const payload = await telephonyConnector.hold(message.data.call);
publishEvent({eventType: constants.VOICE_EVENT_TYPE.HOLD_TOGGLE, payload});
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.HOLD);
} else {
switch(getErrorType(e)) {
case constants.VOICE_ERROR_TYPE.INVALID_PARTICIPANT:
dispatchError(constants.VOICE_ERROR_TYPE.INVALID_PARTICIPANT, getErrorMessage(e), constants.VOICE_MESSAGE_TYPE.HOLD);
break;
default:
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_HOLD_CALL, getErrorMessage(e), constants.VOICE_MESSAGE_TYPE.HOLD);
break;
}
}
}
break;
case constants.VOICE_MESSAGE_TYPE.RESUME:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const payload = await telephonyConnector.resume(message.data.call);
publishEvent({eventType: constants.VOICE_EVENT_TYPE.HOLD_TOGGLE, payload});
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.RESUME);
} else {
switch(getErrorType(e)) {
case constants.VOICE_ERROR_TYPE.INVALID_PARTICIPANT:
dispatchError(constants.VOICE_ERROR_TYPE.INVALID_PARTICIPANT, getErrorMessage(e), constants.VOICE_MESSAGE_TYPE.RESUME);
break;
default:
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_RESUME_CALL, getErrorMessage(e), constants.VOICE_MESSAGE_TYPE.RESUME);
break;
}
}
}
break;
case constants.SHARED_MESSAGE_TYPE.SET_AGENT_STATUS:
try {
const statusInfo = message.data.statusInfo || {};
const enqueueNextState = message.data.enqueueNextState || false;
const payload = await vendorConnector.setAgentStatus(message.data.agentStatus, statusInfo, enqueueNextState);
Validator.validateClassObject(payload, GenericResult, SetAgentStateResult);
const { success, isStatusSyncNeeded } = payload;
dispatchEvent(constants.SHARED_EVENT_TYPE.SET_AGENT_STATUS_RESULT,
isStatusSyncNeeded !== undefined ? { success, isStatusSyncNeeded } : { success });
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.SHARED_MESSAGE_TYPE.SET_AGENT_STATUS);
} else {
if (message.data.statusInfo) {
dispatchEvent(constants.SHARED_EVENT_TYPE.SET_AGENT_STATUS_RESULT, { success: false });
}
switch(getErrorType(e)) {
case constants.SHARED_ERROR_TYPE.INVALID_AGENT_STATUS:
dispatchError(constants.SHARED_ERROR_TYPE.INVALID_AGENT_STATUS, getErrorMessage(e), constants.SHARED_MESSAGE_TYPE.SET_AGENT_STATUS);
break;
default:
dispatchError(constants.SHARED_ERROR_TYPE.CAN_NOT_SET_AGENT_STATUS, getErrorMessage(e), constants.SHARED_MESSAGE_TYPE.SET_AGENT_STATUS);
break;
}
}
}
break;
case constants.SHARED_MESSAGE_TYPE.GET_AGENT_STATUS:
try {
const payload = await vendorConnector.getAgentStatus();
Validator.validateClassObject(payload, AgentVendorStatusInfo);
dispatchEvent(constants.SHARED_EVENT_TYPE.GET_AGENT_STATUS_RESULT, payload);
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.SHARED_MESSAGE_TYPE.GET_AGENT_STATUS);
} else {
dispatchError(constants.SHARED_ERROR_TYPE.CAN_NOT_GET_AGENT_STATUS, getErrorMessage(e), constants.SHARED_MESSAGE_TYPE.GET_AGENT_STATUS);
}
}
break;
case constants.VOICE_MESSAGE_TYPE.DIAL:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const isCallback = message.data.params && message.data.params.indexOf(constants.DIAL_OPTIONS.CALLBACK) >= 0;
const isConsultCall = message.data.params && message.data.params.indexOf(constants.DIAL_OPTIONS.CONSULT) >= 0;
const payload = await telephonyConnector.dial(new Contact(message.data.contact),
new DialOptions({ isCallback, isConsultCall }));
Validator.validateClassObject(payload, CallResult);
const { call } = payload;
// If connectors wants this to be created as callback
if (constants.CALL_TYPE.DIALED_CALLBACK.toLowerCase() === call.callType.toLowerCase() && isCallback) {
dispatchEvent(constants.VOICE_EVENT_TYPE.QUEUED_CALL_STARTED, call);
} else { // continue treating this as outbound
dispatchEvent(constants.VOICE_EVENT_TYPE.CALL_STARTED, call);
}
} catch (e) {
dispatchEvent(constants.VOICE_EVENT_TYPE.CALL_FAILED);
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.DIAL);
} else {
switch(getErrorType(e)) {
case constants.VOICE_ERROR_TYPE.INVALID_DESTINATION:
dispatchError(constants.VOICE_ERROR_TYPE.INVALID_DESTINATION, getErrorMessage(e), constants.VOICE_MESSAGE_TYPE.DIAL);
break;
case constants.SHARED_ERROR_TYPE.GENERIC_ERROR:
dispatchError(constants.SHARED_ERROR_TYPE.GENERIC_ERROR, getErrorMessage(e), constants.VOICE_MESSAGE_TYPE.DIAL);
break;
default:
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_START_THE_CALL, getErrorMessage(e), constants.VOICE_MESSAGE_TYPE.DIAL);
break;
}
}
}
break;
case constants.VOICE_MESSAGE_TYPE.SEND_DIGITS:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
await telephonyConnector.sendDigits(message.data.digits);
} catch (e) {
dispatchEventLog(constants.VOICE_MESSAGE_TYPE.SEND_DIGITS, message.data.digits, true);
}
break;
case constants.VOICE_MESSAGE_TYPE.GET_PHONE_CONTACTS:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const payload = await telephonyConnector.getPhoneContacts(message.data.filter);
Validator.validateClassObject(payload, PhoneContactsResult);
const contacts = payload.contacts.map((contact) => {
return {
id: contact.id,
type: contact.type,
name: contact.name,
listType: contact.listType,
phoneNumber: contact.phoneNumber,
prefix: contact.prefix,
extension: contact.extension,
endpointARN: contact.endpointARN,
queue: contact.queue,
availability: contact.availability,
queueWaitTime: contact.queueWaitTime,
recordId: contact.recordId,
description: contact.description
};
});
dispatchEvent(constants.VOICE_EVENT_TYPE.PHONE_CONTACTS, {
contacts, contactTypes: payload.contactTypes
});
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.GET_PHONE_CONTACTS);
} else {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_GET_PHONE_CONTACTS, e, constants.VOICE_MESSAGE_TYPE.GET_PHONE_CONTACTS);
}
}
break;
case constants.SHARED_MESSAGE_TYPE.GET_CONTACTS:
try {
const payload = await vendorConnector.getContacts(message.data.filter, message.data.workItemId);
Validator.validateClassObject(payload, ContactsResult);
const contacts = payload.contacts.map((contact) => {
return {
id: contact.id,
type: contact.type,
name: contact.name,
listType: contact.listType,
phoneNumber: contact.phoneNumber,
prefix: contact.prefix,
extension: contact.extension,
endpointARN: contact.endpointARN,
queue: contact.queue,
availability: contact.availability,
queueWaitTime: contact.queueWaitTime,
recordId: contact.recordId,
description: contact.description
};
});
dispatchEvent(constants.SHARED_EVENT_TYPE.GET_CONTACTS_RESULT, {
contacts, contactTypes: payload.contactTypes
});
} catch (e) {
dispatchCustomError(e, constants.SHARED_MESSAGE_TYPE.GET_CONTACTS);
}
break;
case constants.VOICE_MESSAGE_TYPE.SWAP_PARTICIPANTS:
try {
// TODO: Create PhoneCall from call1.callId & call2.callId
// TODO: rename to call1 and call2
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const payload = await telephonyConnector.swap(message.data.callToHold, message.data.callToResume);
publishEvent({ eventType: constants.VOICE_EVENT_TYPE.PARTICIPANTS_SWAPPED, payload });
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.SWAP_PARTICIPANTS);
} else {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_SWAP_PARTICIPANTS, e, constants.VOICE_MESSAGE_TYPE.SWAP_PARTICIPANTS);
}
}
break;
case constants.VOICE_MESSAGE_TYPE.CONFERENCE:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const payload = await telephonyConnector.conference(message.data.calls);
publishEvent({ eventType: constants.VOICE_EVENT_TYPE.PARTICIPANTS_CONFERENCED, payload });
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.CONFERENCE);
} else {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_CONFERENCE, e, constants.VOICE_MESSAGE_TYPE.CONFERENCE);
}
}
break;
case constants.VOICE_MESSAGE_TYPE.ADD_PARTICIPANT:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const payload = await telephonyConnector.addParticipant(new Contact(message.data.contact), message.data.call, message.data.isBlindTransfer);
publishEvent({ eventType: constants.VOICE_EVENT_TYPE.PARTICIPANT_ADDED, payload });
if (message.data.isBlindTransfer) {
dispatchEvent(constants.VOICE_EVENT_TYPE.HANGUP, message.data.call);
}
} catch (e) {
// TODO: Can we avoid passing in reason field
dispatchEvent(constants.VOICE_EVENT_TYPE.PARTICIPANT_REMOVED, {
reason: constants.SHARED_EVENT_TYPE.ERROR.toLowerCase()
});
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.ADD_PARTICIPANT);
} else {
switch(getErrorType(e)) {
case constants.VOICE_ERROR_TYPE.INVALID_DESTINATION:
dispatchError(constants.VOICE_ERROR_TYPE.INVALID_DESTINATION, getErrorMessage(e), constants.VOICE_MESSAGE_TYPE.ADD_PARTICIPANT);
break;
default:
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_ADD_PARTICIPANT, getErrorMessage(e), constants.VOICE_MESSAGE_TYPE.ADD_PARTICIPANT);
break;
}
}
}
break;
case constants.VOICE_MESSAGE_TYPE.PAUSE_RECORDING:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const payload = await telephonyConnector.pauseRecording(message.data.call);
publishEvent({ eventType: constants.VOICE_EVENT_TYPE.RECORDING_TOGGLE, payload });
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.PAUSE_RECORDING);
} else {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_PAUSE_RECORDING, e, constants.VOICE_MESSAGE_TYPE.PAUSE_RECORDING);
}
}
break;
case constants.VOICE_MESSAGE_TYPE.RESUME_RECORDING:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const payload = await telephonyConnector.resumeRecording(message.data.call);
publishEvent({ eventType: constants.VOICE_EVENT_TYPE.RECORDING_TOGGLE, payload });
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.RESUME_RECORDING);
} else {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_RESUME_RECORDING, e, constants.VOICE_MESSAGE_TYPE.RESUME_RECORDING);
}
}
break;
case constants.SHARED_MESSAGE_TYPE.LOGOUT:
try {
const payload = await vendorConnector.logout();
Validator.validateClassObject(payload, LogoutResult);
const { success, loginFrameHeight } = payload;
dispatchEvent(constants.SHARED_EVENT_TYPE.LOGOUT_RESULT, { success, loginFrameHeight });
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.SHARED_MESSAGE_TYPE.LOGOUT);
} else {
dispatchError(constants.SHARED_ERROR_TYPE.CAN_NOT_LOG_OUT, e, constants.SHARED_MESSAGE_TYPE.LOGOUT);
}
}
break;
case constants.SHARED_MESSAGE_TYPE.MESSAGE:
// TODO: Define a return type for handling message
vendorConnector.handleMessage(message.data.message);
break;
case constants.VOICE_MESSAGE_TYPE.WRAP_UP_CALL: {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
telephonyConnector.wrapUpCall(message.data.call);
}
break;
case constants.VOICE_MESSAGE_TYPE.AGENT_AVAILABLE: {
if (message.data && message.data.isAvailable) {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const activeCallsResult = await telephonyConnector.getActiveCalls();
Validator.validateClassObject(activeCallsResult, ActiveCallsResult);
const activeCalls = activeCallsResult.activeCalls;
for (const callId in activeCalls) {
const call = activeCalls[callId];
const shouldReplay = call.callInfo ? call.callInfo.isReplayable : true;
const isSupervisorCall = call.callAttributes && call.callAttributes.participantType === constants.PARTICIPANT_TYPE.SUPERVISOR;
const hasSupervisorBargedIn = isSupervisorCall && call.callAttributes && call.callAttributes.hasSupervisorBargedIn;
if (shouldReplay) {
call.isReplayedCall = true;
switch(call.state) {
case constants.CALL_STATE.CONNECTED:
if (isSupervisorCall) {
isSupervisorConnected = true;
dispatchEvent(constants.VOICE_EVENT_TYPE.SUPERVISOR_CALL_CONNECTED, call);
if (hasSupervisorBargedIn) {
dispatchEvent(constants.VOICE_EVENT_TYPE.SUPERVISOR_BARGED_IN, call);
}
break;
}
dispatchEvent(constants.VOICE_EVENT_TYPE.CALL_CONNECTED, call);
break;
case constants.CALL_STATE.RINGING:
if (isSupervisorCall) {
isSupervisorConnected = true;
dispatchEvent(constants.VOICE_EVENT_TYPE.SUPERVISOR_CALL_STARTED, call);
break;
}
dispatchEvent(constants.VOICE_EVENT_TYPE.CALL_STARTED, call);
break;
case constants.CALL_STATE.TRANSFERRING:
dispatchEvent(constants.VOICE_EVENT_TYPE.PARTICIPANT_ADDED, {
phoneNumber: call.contact.phoneNumber,
contact:call.contact,
callInfo: call.callInfo,
callAttributes: call.callAttributes,
initialCallHasEnded: call.callAttributes.initialCallHasEnded,
callId: call.callId,
connectionId: call.connectionId
});
break;
case constants.CALL_STATE.TRANSFERRED:
dispatchEvent(constants.VOICE_EVENT_TYPE.PARTICIPANT_CONNECTED, {
phoneNumber: call.contact.phoneNumber,
contact:call.contact,
callInfo: call.callInfo,
callAttributes: call.callAttributes,
initialCallHasEnded: call.callAttributes.initialCallHasEnded,
callId: call.callId,
connectionId: call.connectionId
});
break;
default:
break;
}
}
}
}
}
break;
case constants.VOICE_MESSAGE_TYPE.SET_AGENT_CONFIG:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const result = await telephonyConnector.setAgentConfig(message.data.config);
Validator.validateClassObjects(result, GenericResult, SetAgentConfigResult);
if (result instanceof SetAgentConfigResult) {
result.setIsSystemEvent(!!message.data.config.isSystemEvent);
}
dispatchEvent(constants.VOICE_EVENT_TYPE.AGENT_CONFIG_UPDATED, result);
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.SET_AGENT_CONFIG);
} else {
dispatchError(getErrorType(e) === constants.VOICE_ERROR_TYPE.CAN_NOT_UPDATE_PHONE_NUMBER ? constants.VOICE_ERROR_TYPE.CAN_NOT_UPDATE_PHONE_NUMBER : constants.VOICE_ERROR_TYPE.CAN_NOT_SET_AGENT_CONFIG , getErrorMessage(e), constants.VOICE_MESSAGE_TYPE.SET_AGENT_CONFIG);
}
}
break;
case constants.VOICE_MESSAGE_TYPE.GET_AUDIO_DEVICES:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const result = await telephonyConnector.getAudioDevices();
Validator.validateClassObject(result, AudioDevicesResult);
dispatchEvent(constants.VOICE_EVENT_TYPE.GET_AUDIO_DEVICES, result);
} catch (e) {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_GET_AUDIO_DEVICES, getErrorMessage(e), constants.VOICE_MESSAGE_TYPE.GET_AUDIO_DEVICES);
}
break;
case constants.VOICE_MESSAGE_TYPE.GET_SIGNED_RECORDING_URL:
try {
const { recordingUrl, vendorCallKey, callId } = message.data;
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const result = await telephonyConnector.getSignedRecordingUrl(recordingUrl, vendorCallKey, callId);
Validator.validateClassObject(result, SignedRecordingUrlResult);
dispatchEvent(constants.VOICE_EVENT_TYPE.SIGNED_RECORDING_URL, result);
} catch (e) {
// In case of an error, we want to show an error message in the recording player
const signedRecordingUrlResult = new SignedRecordingUrlResult({
success: false
});
dispatchEvent(constants.VOICE_EVENT_TYPE.SIGNED_RECORDING_URL, signedRecordingUrlResult, false);
dispatchEventLog(constants.VOICE_MESSAGE_TYPE.GET_SIGNED_RECORDING_URL, signedRecordingUrlResult, true);
}
break;
case constants.SHARED_MESSAGE_TYPE.DOWNLOAD_VENDOR_LOGS:
vendorConnector.downloadLogs(getLogs());
break;
case constants.SHARED_MESSAGE_TYPE.LOG: {
const { logLevel, logMessage, payload } = message.data;
vendorConnector.logMessageToVendor(logLevel, logMessage, payload);
}
break;
case constants.VOICE_MESSAGE_TYPE.SUPERVISE_CALL:
try {
isSupervisorConnected = true;
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const result = await telephonyConnector.superviseCall(message.data.call);
Validator.validateClassObject(result, SuperviseCallResult);
const agentConfigResult = await telephonyConnector.getAgentConfig();
if(agentConfigResult.selectedPhone.type === constants.PHONE_TYPE.SOFT_PHONE) {
dispatchEvent(constants.VOICE_EVENT_TYPE.SUPERVISOR_CALL_CONNECTED, result.call);
} else {
dispatchEvent(constants.VOICE_EVENT_TYPE.SUPERVISOR_CALL_STARTED, result.call);
}
} catch (e){
isSupervisorConnected = false;
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.SUPERVISE_CALL);
} else {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_SUPERVISE_CALL, e, constants.VOICE_MESSAGE_TYPE.SUPERVISE_CALL);
}
}
break;
case constants.VOICE_MESSAGE_TYPE.SUPERVISOR_DISCONNECT:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const result = await telephonyConnector.supervisorDisconnect(message.data.call);
Validator.validateClassObject(result, SupervisorHangupResult);
isSupervisorConnected = false;
dispatchEvent(constants.VOICE_EVENT_TYPE.SUPERVISOR_HANGUP, result.calls);
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.SUPERVISOR_DISCONNECT);
} else {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_DISCONNECT_SUPERVISOR, e, constants.VOICE_MESSAGE_TYPE.SUPERVISOR_DISCONNECT);
}
}
break;
case constants.VOICE_MESSAGE_TYPE.SUPERVISOR_BARGE_IN:
try {
const telephonyConnector = await vendorConnector.getTelephonyConnector();
const result = await telephonyConnector.supervisorBargeIn(message.data.call);
Validator.validateClassObject(result, SuperviseCallResult);
dispatchEvent(constants.VOICE_EVENT_TYPE.SUPERVISOR_BARGED_IN, result.call );
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.VOICE_MESSAGE_TYPE.SUPERVISOR_BARGE_IN);
} else {
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_BARGE_IN_SUPERVISOR, e, constants.VOICE_MESSAGE_TYPE.SUPERVISOR_BARGE_IN);
}
}
break;
case constants.SHARED_MESSAGE_TYPE.AGENT_WORK_EVENT: {
let { workItemId, workId, workEvent } = message.data.agentWork;
vendorConnector.onAgentWorkEvent({
workItemId,
workId,
workEvent
});
}
break;
default:
break;
}
}
async function windowMessageHandler(message) {
switch (message.data.type) {
case constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR: {
const sfDomain = /^https:\/\/[\w-.]+(lightning\.[\w]+\.soma\.force\.com|\.lightning\.force\.com|\.lightning\.pc-rnd\.force\.com|\.stm\.force\.com|\.vf\.force\.com|\.salesforce\.com|\.my\.salesforce-sites\.com|\.lightning\.localhost\.[\w]+\.force.com|\.lightning\.force-com\.[\w.-]+\.crm\.dev|\.[\w-]+\.(salesforce|crmforce)\.mil|\.lightning\.(salesforce|crmforce)\.mil|\.sandbox\.lightning\.(salesforce|crmforce)\.mil)$/;
const originUrl = new URL(message.origin);
const url = originUrl.protocol + '//' + originUrl.hostname;
if (sfDomain.test(url)) {
channelPort = message.ports[0];
channelPort.onmessage = channelMessageHandler;
dispatchEventLog(constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR, exposedConnectorConfig(message.data.connectorConfig), false);
try {
const payload = await vendorConnector.init(message.data.connectorConfig);
Validator.validateClassObject(payload, InitResult);
if (payload.showStorageAccess) {
dispatchEvent(constants.SHARED_EVENT_TYPE.SHOW_STORAGE_ACCESS, {
success: true
});
} else if (payload.showLogin) {
dispatchEvent(constants.SHARED_EVENT_TYPE.SHOW_LOGIN, {
loginFrameHeight: payload.loginFrameHeight
});
} else if (payload.isSilentLogin) {
dispatchEvent(constants.SHARED_EVENT_TYPE.SHOW_LOGIN, {
isSilentLogin: payload.isSilentLogin
});
} else {
setConnectorReady();
}
} catch (e) {
if (e instanceof CustomError) {
dispatchCustomError(e, constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR);
} else {
switch(getErrorType(e)) {
case constants.VOICE_ERROR_TYPE.INVALID_PARAMS:
dispatchError(constants.VOICE_ERROR_TYPE.INVALID_PARAMS, getErrorMessage(e), constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR);
break;
default:
dispatchError(constants.SHARED_ERROR_TYPE.CAN_NOT_LOG_IN, getErrorMessage(e), constants.SHARED_MESSAGE_TYPE.SETUP_CONNECTOR);
break;
}
}
}
}
window.removeEventListener('message', windowMessageHandler);
}
break;
default:
break;
}
}
function exposedConnectorConfig(payload) {
payload = payload || {};
let obj = {};
//properties that are equal to key
CONNECTOR_CONFIG_EXPOSED_FIELDS.forEach(prop => {
if (payload.hasOwnProperty(prop)) {
obj[prop] = payload[prop];
}
});
//properties that start with key
CONNECTOR_CONFIG_EXPOSED_FIELDS_STARTSWITH.forEach(prop => {
Object.keys(payload).forEach(key => {
if (key.startsWith(prop) && !CONNECTOR_CONFIG_EXCEPTION_FIELDS.includes(key)) {
obj[key] = payload[key];
}
});
});
return obj;
}
function validatePayload(payload, payloadType, errorType, eventType) {
try {
Validator.validateClassObject(payload, payloadType);
return true;
} catch (e) {
if (errorType) {
dispatchError(errorType, e, eventType);
}
return false;
}
}
/*========================== Exported Functions ==========================*/
/**
* Initialize a vendor connector
* @param {VendorConnector} connector
*/
export function initializeConnector(connector) {
vendorConnector = connector;
window.addEventListener('message', windowMessageHandler);
}
/**
* Publish an event or error log to Salesforce
* @param {object} param
* @param {string} param.eventType Any event type to be logged
* @param {object} param.payload Any payload for the log that needs to be logged
* @param {boolean} param.isError
*/
export function publishLog({ eventType, payload, isError }) {
dispatchEventLog(eventType, payload, isError);
}
/**
* Publish a telephony error to Salesforce
* @param {object} param
* @param {("LOGIN_RESULT"|"LOGOUT_RESULT"|"CALL_STARTED"|"QUEUED_CALL_STARTED"|"CALL_CONNECTED"|"HANGUP"|"PARTICIPANT_CONNECTED"|"PARTICIPANT_ADDED"|"PARTICIPANTS_SWAPPED"|"PARTICIPANTS_CONFERENCED"|"MESSAGE"|"MUTE_TOGGLE"|"HOLD_TOGGLE"|"RECORDING_TOGGLE"|"AGENT_ERROR"|"SOFTPHONE_ERROR")} param.eventType Event type to publish.
* @param {object} param.error Error object representing the error
*/
export function publishError({ eventType, error }) {
if (error instanceof CustomError) {
dispatchCustomError(error, eventType);
return;
}
switch(eventType) {
case constants.SHARED_EVENT_TYPE.LOGIN_RESULT:
dispatchError(constants.SHARED_ERROR_TYPE.CAN_NOT_LOG_IN, error, constants.SHARED_EVENT_TYPE.LOGIN_RESULT);
break;
case constants.SHARED_EVENT_TYPE.LOGOUT_RESULT:
dispatchError(constants.SHARED_ERROR_TYPE.CAN_NOT_LOG_OUT, error, constants.SHARED_EVENT_TYPE.LOGOUT_RESULT);
break;
case constants.VOICE_EVENT_TYPE.CALL_STARTED:
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_START_THE_CALL, error, constants.VOICE_EVENT_TYPE.CALL_STARTED);
break;
case constants.VOICE_EVENT_TYPE.QUEUED_CALL_STARTED:
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_START_THE_CALL, error, constants.VOICE_EVENT_TYPE.QUEUED_CALL_STARTED);
break;
case constants.VOICE_EVENT_TYPE.CALL_CONNECTED:
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_START_THE_CALL, error, constants.VOICE_EVENT_TYPE.CALL_CONNECTED);
break;
case constants.VOICE_EVENT_TYPE.HANGUP:
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_END_THE_CALL, error, constants.VOICE_EVENT_TYPE.HANGUP);
break;
case constants.VOICE_EVENT_TYPE.PARTICIPANT_ADDED:
dispatchError(getErrorType(error) === constants.VOICE_ERROR_TYPE.INVALID_PARTICIPANT ? constants.VOICE_ERROR_TYPE.INVALID_PARTICIPANT : constants.VOICE_ERROR_TYPE.CAN_NOT_ADD_PARTICIPANT, error, constants.VOICE_EVENT_TYPE.PARTICIPANT_ADDED);
break;
case constants.VOICE_EVENT_TYPE.PARTICIPANT_CONNECTED:
dispatchError(constants.VOICE_ERROR_TYPE.CAN_NOT_CONNECT_PARTICIPANT, error, constants.VOICE_EVENT_TYPE.PARTICIPANT_CONNECTED);
break;