From 1122f07f2066acc41913536e5f589e47b33150cc Mon Sep 17 00:00:00 2001 From: Priya Date: Tue, 26 Nov 2024 20:03:34 +0530 Subject: [PATCH 01/35] feat(plugin-cc): call accept implemented --- packages/@webex/plugin-cc/src/cc.ts | 3 + .../plugin-cc/src/services/Task/constants.ts | 11 + .../plugin-cc/src/services/Task/contact.ts | 395 ++++++++++++++++++ .../plugin-cc/src/services/Task/index.ts | 52 +++ .../plugin-cc/src/services/Task/types.ts | 289 +++++++++++++ .../src/services/WebCallingService.ts | 50 ++- .../plugin-cc/src/services/config/types.ts | 24 ++ .../@webex/plugin-cc/src/services/core/Err.ts | 23 + .../@webex/plugin-cc/src/services/index.ts | 3 + packages/@webex/plugin-cc/src/types.ts | 9 +- 10 files changed, 847 insertions(+), 12 deletions(-) create mode 100644 packages/@webex/plugin-cc/src/services/Task/constants.ts create mode 100644 packages/@webex/plugin-cc/src/services/Task/contact.ts create mode 100644 packages/@webex/plugin-cc/src/services/Task/index.ts create mode 100644 packages/@webex/plugin-cc/src/services/Task/types.ts diff --git a/packages/@webex/plugin-cc/src/cc.ts b/packages/@webex/plugin-cc/src/cc.ts index a3236fd4a55..cbfc498d8f5 100644 --- a/packages/@webex/plugin-cc/src/cc.ts +++ b/packages/@webex/plugin-cc/src/cc.ts @@ -24,6 +24,7 @@ import {StateChange, Logout} from './services/agent/types'; import {ConnectionService} from './services/core/WebSocket/connection-service'; import {getErrorDetails} from './services/core/Utils'; import {Profile, WelcomeEvent} from './services/config/types'; +import Task from './services/Task'; export default class ContactCenter extends WebexPlugin implements IContactCenter { namespace = 'cc'; @@ -35,6 +36,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter private webCallingService: WebCallingService; private connectionService: ConnectionService; private services: Services; + private task: Task; constructor(...args) { super(...args); @@ -65,6 +67,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter }); this.webCallingService = new WebCallingService(this.$webex, this.$config.callingClientConfig); + this.task = new Task(this.services, this.webCallingService); LoggerProxy.initialize(this.$webex.logger); }); diff --git a/packages/@webex/plugin-cc/src/services/Task/constants.ts b/packages/@webex/plugin-cc/src/services/Task/constants.ts new file mode 100644 index 00000000000..570ba3ec816 --- /dev/null +++ b/packages/@webex/plugin-cc/src/services/Task/constants.ts @@ -0,0 +1,11 @@ +export const TASK_MESSAGE_TYPE = 'RoutingMessage'; +export const TASK_API = '/v1/tasks/'; +export const HOLD = '/hold'; +export const UNHOLD = '/unhold'; +export const CONSULT = '/consult'; +export const TRANSFER = '/transfer'; +export const CONSULT_TRANSFER = '/consult/transfer'; +export const PAUSE = '/record/pause'; +export const RESUME = 'record/resume'; +export const WRAPUP = '/wrapup'; +export const END = '/end'; diff --git a/packages/@webex/plugin-cc/src/services/Task/contact.ts b/packages/@webex/plugin-cc/src/services/Task/contact.ts new file mode 100644 index 00000000000..dd76222677b --- /dev/null +++ b/packages/@webex/plugin-cc/src/services/Task/contact.ts @@ -0,0 +1,395 @@ +import {CC_EVENTS} from '../config/types'; +import {WCC_API_GATEWAY} from '../constants'; +import AqmReqs from '../core/aqm-reqs'; +import {TIMEOUT_REQ} from '../core/constants'; +import { + CONSULT_TRANSFER, + HOLD, + PAUSE, + TASK_API, + TASK_MESSAGE_TYPE, + TRANSFER, + UNHOLD, + WRAPUP, +} from './constants'; +import * as Contact from './types'; +import {DESTINATION_TYPE} from './types'; + +export default function routingContact(aqm: AqmReqs) { + return { + /* + * Accept incoming task + */ + accept: aqm.req((p: {interactionId: string}) => ({ + url: `${TASK_API}${p.interactionId}/accept`, + data: {}, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_CONTACT_ASSIGNED, interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED, interactionId: p.interactionId}, + }, + errId: 'Service.aqm.task.accept', + }, + })), + + /* + * Hold task + */ + hold: aqm.req((p: {interactionId: string; data: Contact.HoldResumePayload}) => ({ + url: `${TASK_API}${p.interactionId}${HOLD}`, + data: p.data, + host: WCC_API_GATEWAY, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_CONTACT_HELD, interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_CONTACT_HOLD_FAILED}, + }, + errId: 'Service.aqm.task.hold', + }, + })), + + /* + * Unhold task + */ + unHold: aqm.req((p: {interactionId: string; data: Contact.HoldResumePayload}) => ({ + url: `${TASK_API}${p.interactionId}${UNHOLD}`, + data: p.data, + host: WCC_API_GATEWAY, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_CONTACT_UNHELD, interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_CONTACT_UNHOLD_FAILED}, + }, + errId: 'Service.aqm.task.unHold', + }, + })), + + /* + * Pause Recording + */ + pauseRecording: aqm.req((p: {interactionId: string}) => ({ + url: `${TASK_API}${p.interactionId}${PAUSE}`, + data: {}, + host: WCC_API_GATEWAY, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.CONTACT_RECORDING_PAUSED, interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.CONTACT_RECORDING_PAUSE_FAILED}, + }, + errId: 'Service.aqm.task.pauseRecording', + }, + })), + + /* + * Resume Recording + */ + resumeRecording: aqm.req( + (p: {interactionId: string; data: Contact.ResumeRecordingPayload}) => ({ + url: `${TASK_API}${p.interactionId}/record/resume`, + data: p.data, + host: WCC_API_GATEWAY, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.CONTACT_RECORDING_RESUMED, interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.CONTACT_RECORDING_RESUME_FAILED}, + }, + errId: 'Service.aqm.task.resumeRecording', + }, + }) + ), + + /* + * Consult contact + */ + consult: this.aqm.req((p: {interactionId: string; data: Contact.ConsultData; url: string}) => ({ + url: `${TASK_API}${p.interactionId}/consult`, + data: p.data, + timeout: + p.data && p.data.destinationType === DESTINATION_TYPE.QUEUE ? 'disabled' : TIMEOUT_REQ, + host: WCC_API_GATEWAY, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_CONSULT_CREATED, interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: { + type: + p.data && p.data.destinationType === DESTINATION_TYPE.QUEUE + ? CC_EVENTS.AGENT_CTQ_FAILED + : CC_EVENTS.AGENT_CONSULT_FAILED, + }, + }, + errId: 'Service.aqm.contact.consult', + }, + notifCancel: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: 'AgentCtqCancelled', interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + })), + + /* + * Consult Accept contact + */ + consultAccept: aqm.req((p: {interactionId: string}) => ({ + url: `${TASK_API}${p.interactionId}/consult/accept`, + data: {}, + host: WCC_API_GATEWAY, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_CONSULTING, interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED}, + }, + errId: 'Service.aqm.task.consultAccept', + }, + })), + + /* + * BlindTransfer contact + */ + blindTransfer: aqm.req((p: {interactionId: string; data: Contact.TransferPayLoad}) => ({ + url: `${TASK_API}${p.interactionId}${TRANSFER}`, + data: p.data, + host: WCC_API_GATEWAY, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_BLIND_TRANSFERRED, interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: 'AgentBlindTransferFailedEvent'}, + }, + errId: 'Service.aqm.task.AgentBlindTransferFailedEvent', + }, + })), + + /* + * VteamTransfer contact + */ + vteamTransfer: aqm.req((p: {interactionId: string; data: Contact.TransferPayLoad}) => ({ + url: `${TASK_API}${p.interactionId}${TRANSFER}`, + data: p.data, + host: WCC_API_GATEWAY, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: 'AgentVteamTransferred', interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: 'AgentVteamTransferFailed'}, + }, + errId: 'Service.aqm.task.AgentVteamTransferFailed', + }, + })), + + /* + * Consult Transfer contact + */ + consultTransfer: aqm.req( + (p: {interactionId: string; data: Contact.ConsultTransferPayLoad}) => ({ + url: `${TASK_API}${p.interactionId}${CONSULT_TRANSFER}`, + data: p.data, + host: WCC_API_GATEWAY, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: { + type: [CC_EVENTS.AGENT_CONSULT_TRANSFERRED, CC_EVENTS.AGENT_CONSULT_TRANSFERRING], + interactionId: p.interactionId, + }, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_CONSULT_TRANSFER_FAILED}, + }, + errId: 'Service.aqm.contact.AgentConsultTransferFailed', + }, + }) + ), + + /* + * End contact + */ + end: this.aqm.req((p: {interactionId: string}) => ({ + url: `${TASK_API}${p.interactionId}/end`, + data: {}, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_WRAPUP, interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_CONTACT_END_FAILED}, + }, + errId: 'Service.aqm.task.end', + }, + })), + + /* + * Wrapup contact + */ + wrapup: aqm.req((p: {interactionId: string; data: Contact.WrapupPayLoad}) => ({ + url: `${TASK_API}${p.interactionId}${WRAPUP}`, + data: p.data, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_WRAPPEDUP, interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENt_WRAPUP_FAILED}, + }, + errId: 'Service.aqm.task.wrapup', + }, + })), + + /* + * Cancel popover + */ + cancelTask: this.aqm.req((p: {interactionId: string}) => ({ + url: `${TASK_API}${p.interactionId}/end`, + data: {}, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.CONTACT_ENDED, interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: CC_EVENTS.AGENT_CONTACT_END_FAILED}, + }, + errId: 'Service.aqm.task.end', + }, + })), + + /* + * Cancel Ctq request + */ + cancelCtq: aqm.req((p: {interactionId: string; data: Contact.cancelCtq}) => ({ + url: `${TASK_API}${p.interactionId}/cancelCtq`, + data: p.data, + err, + notifSuccess: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: 'AgentCtqCancelled', interactionId: p.interactionId}, + }, + msg: {} as Contact.AgentContact, + }, + notifFail: { + bind: { + type: TASK_MESSAGE_TYPE, + data: {type: 'AgentCtqCancelFailed'}, + }, + errId: 'Service.aqm.task.cancelCtq', + }, + })), + }; +} + +// /* +// * Get list of queues available. +// */ +// vteamList: aqm.req((p: { data: Contact.VTeam }) => ({ +// url: `/vteams`, +// data: p.data, +// err, +// notifSuccess: { +// bind: { +// type: "VteamList", +// data: { jsMethod: "vteamListChanged" } +// }, +// msg: {} as Contact.VTeamSuccess +// }, +// notifFail: { +// bind: { +// type: "VteamListFailed", +// data: { statusCode: 500 } +// }, +// errId: "Service.aqm.task.VteamListFailed" +// } +// })), diff --git a/packages/@webex/plugin-cc/src/services/Task/index.ts b/packages/@webex/plugin-cc/src/services/Task/index.ts new file mode 100644 index 00000000000..e560c699bde --- /dev/null +++ b/packages/@webex/plugin-cc/src/services/Task/index.ts @@ -0,0 +1,52 @@ +import {LocalMicrophoneStream, createMicrophoneStream} from '@webex/calling'; +import Services from '..'; +import {getErrorDetails} from '../core/Utils'; +import WebCallingService from '../WebCallingService'; +import {LoginOption} from '../../types'; + +export default class Task { + private localAudioStream: LocalMicrophoneStream; + private services: Services; // This will be used later for invoking Contact APIs + private webCallingService: WebCallingService; + + constructor(services: Services, webCallingService: WebCallingService) { + this.services = services; + this.webCallingService = webCallingService; + } + + /** + * This is used for incoming task accept by agent. + * @param data + * @returns Promise + * @throws Error + */ + public async accept(loginOption: LoginOption, taskId: string): Promise { + try { + if (loginOption === LoginOption.BROWSER) { + this.localAudioStream = await createMicrophoneStream({audio: true}); + this.webCallingService.answerCall(this.localAudioStream, taskId); + } else { + // TODO: Invoke the accept API from services layer. This is going to be used in Outbound Dialer scenario + this.services.contact.accept({interactionId: taskId}); + } + } catch (error) { + throw getErrorDetails(error, 'accept'); + } + } + + /** + * This is used for the incoming task decline by agent. + * @param data + * @returns Promise + * @throws Error + */ + public async decline(taskId: string): Promise { + try { + this.webCallingService.declinecall(taskId); + } catch (error) { + throw getErrorDetails(error, 'decline'); + } + } + + // TODO: Hold/resume, recording pause/resume, consult and transfer public methods to be implemented here +} diff --git a/packages/@webex/plugin-cc/src/services/Task/types.ts b/packages/@webex/plugin-cc/src/services/Task/types.ts new file mode 100644 index 00000000000..7f5f61b9515 --- /dev/null +++ b/packages/@webex/plugin-cc/src/services/Task/types.ts @@ -0,0 +1,289 @@ +import {Msg} from '../core/GlobalTypes'; + +export enum DESTINATION_TYPE { + QUEUE = 'queue', + DIALNUMBER = 'dialNumber', + AGENT = 'agent', + EPDN = 'entrypointDialNumber', + ENTRYPOINT = 'entryPoint', +} + +type DestinationType = + | DESTINATION_TYPE.AGENT + | DESTINATION_TYPE.QUEUE + | DESTINATION_TYPE.DIALNUMBER + | DESTINATION_TYPE.EPDN + | DESTINATION_TYPE.ENTRYPOINT; + +type MEDIA_CHANNEL = + | 'email' + | 'chat' + | 'telephony' + | 'social' + | 'sms' + | 'facebook' + | 'whatsapp' + | string; + +export type AgentContact = Msg<{ + mediaResourceId: string; + eventType: string; + eventTime?: number; + agentId: string; + destAgentId: string; + trackingId: string; + consultMediaResourceId: string; + interaction: Interaction; + participantId?: string; + fromOwner?: boolean; + toOwner?: boolean; + childInteractionId?: string; + interactionId: string; + orgId: string; + owner: string; + queueMgr: string; + queueName?: string; + type: string; + ronaTimeout?: number; + isConsulted?: boolean; + isConferencing: boolean; + updatedBy?: string; + destinationType?: string; + autoResumed?: boolean; + reasonCode?: string | number; + reason?: string; + consultingAgentId?: string; + taskId?: string; + task?: Interaction; + supervisorId?: string; + monitorType?: string; + supervisorDN?: string; + id?: string; // unique id in monitoring offered event + isWebCallMute?: boolean; + reservationInteractionId?: string; + reservedAgentChannelId?: string; + monitoringState?: { + type: string; + }; + supervisorName?: string; +}>; + +export type Contact = { + /** Contact start time in timestamp */ + cstts: string; + /** Contact end time in timestamp */ + cetts: string; + /** talk duration in timestamp */ + talkDuration: number; + agentName: string; + /** entry point of the contact */ + entrypointName?: string; + /** Channel type pof the contact e.g. email|chat|telephony */ + channelTypeexport: string; + /** ani of the customer */ + ani: string; + displayAni: string; + sid: string; + /** transcript id */ + transcript?: string; + /** outbound transcript id */ + outboundTranscript?: string; + terminationType?: string; + /** Contact Subject */ + subject?: string; + customerName: string; + dnis: string; + callDirection: string; + subChannelType?: string; + wrapUpCode: string; + isCallback?: boolean; + outdialType?: string; +}; + +export type VTeam = { + agentProfileId: string; + agentSessionId: string; + channelType: string; + type: string; + trackingId?: string; +}; + +export type VteamDetails = { + name: string; + channelType: string; + id: string; + type: string; + analyzerId: string; +}; + +export type VTeamSuccess = Msg<{ + data: { + vteamList: Array; + allowConsultToQueue: boolean; + }; + jsMethod: string; + callData: string; + agentSessionId: string; +}>; + +export type Interaction = { + isFcManaged: boolean; + isTerminated: boolean; + mediaType: MEDIA_CHANNEL; + previousVTeams: string[]; + state: string; + currentVTeam: string; + participants: any; // todo + interactionId: string; + orgId: string; + createdTimestamp?: number; + isWrapUpAssist?: boolean; + + callProcessingDetails: { + QMgrName: string; + taskToBeSelfServiced: string; + ani: string; + displayAni: string; + dnis: string; + tenantId: string; + QueueId: string; + vteamId: string; + pauseResumeEnabled?: string; + pauseDuration?: string; + isPaused?: string; + recordInProgress?: string; + recordingStarted?: string; + ctqInProgress?: string; + outdialTransferToQueueEnabled?: string; + convIvrTranscript?: string; + customerName: string; + virtualTeamName: string; + ronaTimeout: string; + category: string; + reason: string; + sourceNumber: string; + sourcePage: string; + appUser: string; + customerNumber: string; + reasonCode: string; + IvrPath: string; + pathId: string; + fromAddress: string; + parentInteractionId?: string; + childInteractionId?: string; + relationshipType?: string; + parent_ANI?: string; + parent_DNIS?: string; + consultDestinationAgentJoined?: boolean | string; + consultDestinationAgentName?: string; + parent_Agent_DN?: string; + parent_Agent_Name?: string; + parent_Agent_TeamName?: string; + isConferencing?: string; + monitorType?: string; + workflowName?: string; + workflowId?: string; + monitoringInvisibleMode?: string; + monitoringRequestId?: string; + participantInviteTimeout?: string; + mohFileName?: string; + CONTINUE_RECORDING_ON_TRANSFER?: string; + EP_ID?: string; + ROUTING_TYPE?: string; + fceRegisteredEvents?: string; + isParked?: string; + priority?: string; + routingStrategyId?: string; + monitoringState?: string; + BLIND_TRANSFER_IN_PROGRESS?: boolean; + fcDesktopView?: string; + }; + mainInteractionId?: string; + media: Record< + string, + { + mediaResourceId: string; + mediaType: MEDIA_CHANNEL; + mediaMgr: string; + participants: string[]; + mType: string; + isHold: boolean; + holdTimestamp: number | null; + } + >; + owner: string; + mediaChannel: MEDIA_CHANNEL; + contactDirection: {type: string}; + outboundType?: string; + callFlowParams: Record< + string, + { + name: string; + qualifier: string; + description: string; + valueDataType: string; + value: string; + } + >; +}; + +export type HoldResumePayload = { + mediaResourceId: string; +}; + +export type ResumeRecordingPayload = { + autoResumed: boolean; +}; + +export type TransferPayLoad = { + to: string; + destinationType: DestinationType; +}; + +export type ConsultTransferPayLoad = { + to: string; + destinationType: DestinationType; +}; + +export type ConsultData = { + to: string | undefined; + destinationType: string; + holdParticipants?: boolean; +}; + +export type ConsultConferenceData = { + agentId?: string; + to: string | undefined; + destinationType: string; +}; + +export type cancelCtq = { + agentId: string; + queueId: string; +}; + +export type declinePayload = { + mediaResourceId: string; +}; + +export type WrapupPayLoad = { + wrapUpReason: string; + auxCodeId: string; +}; + +export type ContactCleanupData = { + type: string; + orgId: string; + agentId: string; + data: { + eventType: string; + interactionId: string; + orgId: string; + mediaMgr: string; + trackingId: string; + mediaType: string; + destination?: string; + broadcast: boolean; + type: string; + }; +}; diff --git a/packages/@webex/plugin-cc/src/services/WebCallingService.ts b/packages/@webex/plugin-cc/src/services/WebCallingService.ts index 27154131ac7..d6718b3c60d 100644 --- a/packages/@webex/plugin-cc/src/services/WebCallingService.ts +++ b/packages/@webex/plugin-cc/src/services/WebCallingService.ts @@ -5,6 +5,7 @@ import { ILine, LINE_EVENTS, CallingClientConfig, + LocalMicrophoneStream, } from '@webex/calling'; import {WebexSDK} from '../types'; import {TIMEOUT_DURATION} from '../constants'; @@ -25,20 +26,12 @@ export default class WebCallingService { this.line = Object.values(this.callingClient.getLines())[0]; this.line.on(LINE_EVENTS.UNREGISTERED, () => { - this.webex.logger.log(`WxCC-SDK: Desktop un registered successfully`); + this.webex.logger.log(`WxCC-SDK: Desktop unregistered successfully`); }); // Start listening for incoming calls - this.line.on(LINE_EVENTS.INCOMING_CALL, (callObj: ICall) => { - this.call = callObj; - - const incomingCallEvent = new CustomEvent(LINE_EVENTS.INCOMING_CALL, { - detail: { - call: this.call, - }, - }); - - window.dispatchEvent(incomingCallEvent); + this.line.on(LINE_EVENTS.INCOMING_CALL, (call: ICall) => { + this.call = call; }); return new Promise((resolve, reject) => { @@ -60,4 +53,39 @@ export default class WebCallingService { public async deregisterWebCallingLine() { this.line?.deregister(); } + + public answerCall(localAudioStream: LocalMicrophoneStream, taskId: string) { + if (this.call) { + this.webex.logger.info(`[WebRtc]: Call answered: ${taskId}`); + this.call.answer(localAudioStream); + } else { + this.webex.logger.log(`[WebRtc]: Cannot answer a non WebRtc Call: ${taskId}`); + } + } + + public muteCall(localAudioStream: LocalMicrophoneStream) { + if (this.call) { + this.webex.logger.info('[WebRtc]: Call mute|unmute requesting!'); + this.call.mute(localAudioStream); + } else { + this.webex.logger.log(`[WebRtc]: Cannot mute a non WebRtc Call`); + } + } + + public isCallMuted() { + if (this.call) { + return this.call.isMuted(); + } + + return false; + } + + public declinecall(taskId: string) { + if (this.call) { + this.webex.logger.info(`[WebRtc]: Call end requested: ${taskId}`); + this.call.end(); + } else { + this.webex.logger.log(`[WebRtc]: Cannot mute a non WebRtc Call: ${taskId}`); + } + } } diff --git a/packages/@webex/plugin-cc/src/services/config/types.ts b/packages/@webex/plugin-cc/src/services/config/types.ts index 18f10273a40..128072d406b 100644 --- a/packages/@webex/plugin-cc/src/services/config/types.ts +++ b/packages/@webex/plugin-cc/src/services/config/types.ts @@ -19,6 +19,30 @@ export const CC_EVENTS = { AGENT_BUDDY_AGENTS: 'BuddyAgents', AGENT_BUDDY_AGENTS_SUCCESS: 'BuddyAgents', AGENT_BUDDY_AGENTS_RETRIEVE_FAILED: 'BuddyAgentsRetrieveFailed', + AGENT_CONTACT_ASSIGNED: 'AgentContactAssigned', + AGENT_CONTACT_ASSIGN_FAILED: 'AgentContactAssignFailed', + AGENT_CONTACT_HELD: 'AgentContactHeld', + AGENT_CONTACT_HOLD_FAILED: 'AgentContactHoldFailed', + AGENT_CONTACT_UNHELD: 'AgentContactUnheld', + AGENT_CONTACT_UNHOLD_FAILED: 'AgentContactUnHoldFailed', + AGENT_CONSULT_CREATED: 'AgentConsultCreated', + AGENT_CONSULTING: 'AgentConsulting', + AGENT_CONSULT_FAILED: 'AgentConsultFailed', + AGENT_CTQ_FAILED: 'AgentCtqFailed', + AGENT_CONSULT_ENDED: 'AgentConsultEnded', + AGENT_BLIND_TRANSFERRED: 'AgentBlindTransferred', + AGENT_CONSULT_TRANSFERRING: 'AgentConsultTransferring', + AGENT_CONSULT_TRANSFERRED: 'AgentConsultTransferred', + AGENT_CONSULT_TRANSFER_FAILED: 'AgentConsultTransferFailed', + CONTACT_RECORDING_PAUSED: 'ContactRecordingPaused', + CONTACT_RECORDING_PAUSE_FAILED: 'ContactRecordingPauseFailed', + CONTACT_RECORDING_RESUMED: 'ContactRecordingResumed', + CONTACT_RECORDING_RESUME_FAILED: 'ContactRecordingResumeFailed', + CONTACT_ENDED: 'ContactEnded', + AGENT_CONTACT_END_FAILED: 'AgentContactEndFailed', + AGENT_WRAPUP: 'AgentWrapup', + AGENT_WRAPPEDUP: 'AgentWrappedUp', + AGENT_WRAPUP_FAILED: 'AgentWrapupFailed', } as const; export type WelcomeEvent = { diff --git a/packages/@webex/plugin-cc/src/services/core/Err.ts b/packages/@webex/plugin-cc/src/services/core/Err.ts index ef3c1a7c500..8d94d369fd9 100644 --- a/packages/@webex/plugin-cc/src/services/core/Err.ts +++ b/packages/@webex/plugin-cc/src/services/core/Err.ts @@ -12,6 +12,28 @@ export type AgentErrorIds = | {'Service.reqs.generic.failure': {trackingId: string}} | {'Service.aqm.agent.BuddyAgentsRetrieveFailed': Failure}; +export type vteamType = 'inboundqueue' | 'inboundentrypoint' | string; + +export type TaskErrorIds = + | {'Service.aqm.task.accept': Failure} + | {'Service.aqm.task.end': Failure} + | {'Service.aqm.task.wrapup': Failure} + | {'Service.aqm.task.AgentVteamTransferFailed': Failure} + | {'Service.aqm.task.AgentBlindTransferFailedEvent': Failure} + | {'Service.aqm.task.AgentConsultTransferFailed': Failure} + | {'Service.aqm.task.consult': Failure} + | {'Service.aqm.err.trackingId': {trackingId: string}} + | {'Service.aqm.task.consultAccept': Failure} + | {'Service.aqm.task.consultConference': Failure} + | {'Service.aqm.task.consultEnd': Failure} + | {'Service.aqm.task.cancelCtq': Failure} + | {'Service.aqm.task.hold': Failure} + | {'Service.aqm.task.unHold': Failure} + | {'Service.aqm.task.VteamListFailed': Failure} + | {'Service.aqm.task.pauseRecording': Failure} + | {'Service.aqm.task.resumeRecording': Failure} + | {'Service.reqs.generic.failure': {trackingId: string}}; + export type ReqError = | 'Service.aqm.reqs.GenericRequestError' | {'Service.aqm.reqs.Pending': {key: string; msg: string}} @@ -22,6 +44,7 @@ export type ReqError = export interface Ids { 'Service.aqm.agent': AgentErrorIds; 'Service.aqm.reqs': ReqError; + 'Service.aqm.task': TaskErrorIds; } export type IdsGlobal = diff --git a/packages/@webex/plugin-cc/src/services/index.ts b/packages/@webex/plugin-cc/src/services/index.ts index 8b842d8786b..a0c12b34a1b 100644 --- a/packages/@webex/plugin-cc/src/services/index.ts +++ b/packages/@webex/plugin-cc/src/services/index.ts @@ -1,4 +1,5 @@ import routingAgent from './agent'; +import routingContact from './Task/contact'; import AgentConfigService from './config'; import AqmReqs from './core/aqm-reqs'; import {WebSocketManager} from './core/WebSocket/WebSocketManager'; @@ -6,6 +7,7 @@ import {WebSocketManager} from './core/WebSocket/WebSocketManager'; export default class Services { public readonly agent: ReturnType; public readonly config: AgentConfigService; + public readonly contact: ReturnType; private static instance: Services; constructor(options: {webSocketManager: WebSocketManager}) { @@ -13,6 +15,7 @@ export default class Services { const aqmReq = new AqmReqs(webSocketManager); this.config = new AgentConfigService(); this.agent = routingAgent(aqmReq); + this.contact = routingContact(aqmReq); } public static getInstance(options: {webSocketManager: WebSocketManager}): Services { diff --git a/packages/@webex/plugin-cc/src/types.ts b/packages/@webex/plugin-cc/src/types.ts index e2a93b71c12..c0a03705e3c 100644 --- a/packages/@webex/plugin-cc/src/types.ts +++ b/packages/@webex/plugin-cc/src/types.ts @@ -1,5 +1,6 @@ import {CallingClientConfig} from '@webex/calling'; import * as Agent from './services/agent/types'; +import * as Contact from './services/Task/types'; import {Profile} from './services/config/types'; type Enum> = T[keyof T]; @@ -200,7 +201,13 @@ export type RequestBody = | Agent.Logout | Agent.UserStationLogin | Agent.StateChange - | Agent.BuddyAgents; + | Agent.BuddyAgents + | Contact.HoldResumePayload + | Contact.ResumeRecordingPayload + | Contact.TransferPayLoad + | Contact.ConsultTransferPayLoad + | Contact.cancelCtq + | Contact.WrapupPayLoad; /** * Represents the options to fetch buddy agents for the logged in agent. From 39a55a4fd7ee3c469dafa538c5208645ae847c5f Mon Sep 17 00:00:00 2001 From: Priya Date: Wed, 27 Nov 2024 00:03:34 +0530 Subject: [PATCH 02/35] feat(plugin-cc): added ut, added sample app changes --- docs/samples/contact-center/app.js | 29 +++ docs/samples/contact-center/index.html | 11 + .../plugin-cc/src/services/Task/contact.ts | 55 +++-- .../plugin-cc/src/services/Task/index.ts | 32 ++- .../src/services/WebCallingService.ts | 5 +- .../plugin-cc/src/services/config/types.ts | 1 + .../core/WebSocket/WebSocketManager.ts | 6 +- .../test/unit/spec/services/Task/contact.ts | 203 ++++++++++++++++++ .../test/unit/spec/services/Task/index.ts | 91 ++++++++ 9 files changed, 397 insertions(+), 36 deletions(-) create mode 100644 packages/@webex/plugin-cc/test/unit/spec/services/Task/contact.ts create mode 100644 packages/@webex/plugin-cc/test/unit/spec/services/Task/index.ts diff --git a/docs/samples/contact-center/app.js b/docs/samples/contact-center/app.js index b9a02172b19..03918694cc6 100644 --- a/docs/samples/contact-center/app.js +++ b/docs/samples/contact-center/app.js @@ -34,6 +34,10 @@ const idleCodesDropdown = document.querySelector('#idleCodesDropdown') const setAgentStatusButton = document.querySelector('#setAgentStatus'); const logoutAgentElm = document.querySelector('#logoutAgent'); const buddyAgentsDropdownElm = document.getElementById('buddyAgentsDropdown'); +const callListener = document.querySelector('#incomingsection'); +const incomingDetailsElm = document.querySelector('#incoming-call'); +const answerElm = document.querySelector('#answer'); +const declineElm = document.querySelector('#decline'); // Store and Grab `access-token` from sessionStorage if (sessionStorage.getItem('date') > new Date().getTime()) { @@ -244,6 +248,31 @@ async function fetchBuddyAgents() { buddyAgentsDropdownElm.innerHTML = `