From cfe40f63f53df9dec70e2eb0c3e2bf3fd2d5b8a5 Mon Sep 17 00:00:00 2001 From: Priya Date: Mon, 2 Dec 2024 18:44:47 +0530 Subject: [PATCH] feat(plugin-cc): review comments --- docs/samples/contact-center/app.js | 33 ++- packages/@webex/plugin-cc/src/cc.ts | 28 ++- .../src/services/TaskControl/index.ts | 111 ---------- .../src/services/WebSocket/config.ts | 47 ---- .../plugin-cc/src/services/WebSocket/index.ts | 70 ------ .../plugin-cc/src/services/WebSocket/types.ts | 45 ---- .../core/WebSocket/connection-service.ts | 2 +- .../src/services/core/WebSocket/types.ts | 2 +- .../plugin-cc/src/services/core/aqm-reqs.ts | 5 +- .../@webex/plugin-cc/src/services/index.ts | 6 +- .../src/services/task/TaskManager.ts | 101 +++++++++ .../{TaskControl => task}/constants.ts | 2 + .../services/{TaskControl => task}/contact.ts | 2 +- .../plugin-cc/src/services/task/index.ts | 83 +++++++ .../services/{TaskControl => task}/types.ts | 205 ++++++++++++------ packages/@webex/plugin-cc/src/types.ts | 2 +- .../@webex/plugin-cc/test/unit/spec/cc.ts | 4 +- .../test/unit/spec/services/Task/contact.ts | 4 +- .../test/unit/spec/services/Task/index.ts | 28 +-- 19 files changed, 385 insertions(+), 395 deletions(-) delete mode 100644 packages/@webex/plugin-cc/src/services/TaskControl/index.ts delete mode 100644 packages/@webex/plugin-cc/src/services/WebSocket/config.ts delete mode 100644 packages/@webex/plugin-cc/src/services/WebSocket/index.ts delete mode 100644 packages/@webex/plugin-cc/src/services/WebSocket/types.ts create mode 100644 packages/@webex/plugin-cc/src/services/task/TaskManager.ts rename packages/@webex/plugin-cc/src/services/{TaskControl => task}/constants.ts (86%) rename packages/@webex/plugin-cc/src/services/{TaskControl => task}/contact.ts (100%) create mode 100644 packages/@webex/plugin-cc/src/services/task/index.ts rename packages/@webex/plugin-cc/src/services/{TaskControl => task}/types.ts (80%) diff --git a/docs/samples/contact-center/app.js b/docs/samples/contact-center/app.js index 11b76ef512b..319a7dfe9bc 100644 --- a/docs/samples/contact-center/app.js +++ b/docs/samples/contact-center/app.js @@ -78,23 +78,16 @@ function toggleDisplay(elementId, status) { } } -const taskControlEvents = new CustomEvent('task:incoming', { +const taskEvents = new CustomEvent('task:incoming', { detail: { task: task, }, }); -function registerListeners(taskControl) { - taskControl.on('task:incoming', (task) => { - task = task; - taskControlEvents.detail.task = task; - - incomingCallListener.dispatchEvent(taskControlEvents); - }) - - taskControl.on('task:assigned', (task) => { +// TODO: Activate the call control buttons once the call is accepted and refctor this +function registerListeners(task) { + task.on('task:assigned', (task) => { console.log('Call has been accepted'); - // TODO: Activate the call control buttons once the call is accepted }) } @@ -188,8 +181,12 @@ function register() { console.error('Event subscription failed', error); }) - taskControl = webex.cc.taskControl; - registerListeners(taskControl); + webex.cc.on('task:incoming', (task) => { + task = task; + taskEvents.detail.task = task; + + incomingCallListener.dispatchEvent(taskEvents); + }) } async function handleAgentLogin(e) { @@ -278,10 +275,10 @@ async function fetchBuddyAgents() { } incomingCallListener.addEventListener('task:incoming', (event) => { - taskId = event.detail.task.interactionId; - const callerDisplay = event.detail.task.interaction.callAssociatedDetails.ani; + taskId = event.detail.task.data.interactionId; + const callerDisplay = event.detail.task.data.interaction.callAssociatedDetails.ani; - if (taskControl.webCallingService.loginOption === 'BROWSER') { + if (task.webCallingService.loginOption === 'BROWSER') { answerElm.disabled = false; declineElm.disabled = false; @@ -294,14 +291,14 @@ incomingCallListener.addEventListener('task:incoming', (event) => { function answer() { answerElm.disabled = true; declineElm.disabled = true; - webex.cc.taskControl.accept(taskId); + task.accept(taskId); incomingDetailsElm.innerText = 'Call Accepted'; } function decline() { answerElm.disabled = true; declineElm.disabled = true; - webex.cc.taskControl.decline(taskId); + task.decline(taskId); incomingDetailsElm.innerText = 'No incoming calls'; } diff --git a/packages/@webex/plugin-cc/src/cc.ts b/packages/@webex/plugin-cc/src/cc.ts index 87afaf34ce0..f6ab69f8a3a 100644 --- a/packages/@webex/plugin-cc/src/cc.ts +++ b/packages/@webex/plugin-cc/src/cc.ts @@ -1,4 +1,5 @@ import {WebexPlugin} from '@webex/webex-core'; +import EventEmitter from 'events'; import { SetStateResponse, CCPluginConfig, @@ -14,7 +15,6 @@ import { SubscribeRequest, } from './types'; import {READY, CC_FILE, EMPTY_STRING} from './constants'; -import WebCallingService from './services/WebCallingService'; import {AGENT, WEB_RTC_PREFIX} from './services/constants'; import Services from './services'; import HttpRequest from './services/core/HttpRequest'; @@ -23,22 +23,26 @@ import {StateChange, Logout} from './services/agent/types'; import {getErrorDetails} from './services/core/Utils'; import {Profile, WelcomeEvent} from './services/config/types'; import {AGENT_STATE_AVAILABLE} from './services/config/constants'; -import {ConnectionLostDetails} from './services/core/WebSocket/types'; -import TaskControl from './services/TaskControl'; +import {ConnectionLostDetails} from './services/core/websocket/types'; +import TaskManager from './services/task/TaskManager'; +import WebCallingService from './services/WebCallingService'; +import {ITask, TASK_EVENTS} from './services/task/types'; export default class ContactCenter extends WebexPlugin implements IContactCenter { namespace = 'cc'; private $config: CCPluginConfig; private $webex: WebexSDK; + private eventEmitter: EventEmitter; private agentConfig: Profile; private webCallingService: WebCallingService; private services: Services; private httpRequest: HttpRequest; - private taskControl: TaskControl; + private taskManager: TaskManager; constructor(...args) { super(...args); + this.eventEmitter = new EventEmitter(); // @ts-ignore this.$webex = this.webex; @@ -59,16 +63,26 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter }); this.webCallingService = new WebCallingService(this.$webex, this.$config.callingClientConfig); - this.taskControl = new TaskControl( + this.taskManager = TaskManager.getTaskManager( this.services.contact, - this.services.webSocketManager, - this.webCallingService + this.webCallingService, + this.services.webSocketManager ); + this.incomingTaskListener(); LoggerProxy.initialize(this.$webex.logger); }); } + /** + * An Incoming Call listener. + */ + private incomingTaskListener() { + this.taskManager.on(TASK_EVENTS.TASK_INCOMING, (task: ITask) => { + this.eventEmitter.emit(TASK_EVENTS.TASK_INCOMING, task); + }); + } + /** * This is used for making the CC SDK ready by setting up the cc mercury connection. */ diff --git a/packages/@webex/plugin-cc/src/services/TaskControl/index.ts b/packages/@webex/plugin-cc/src/services/TaskControl/index.ts deleted file mode 100644 index 17e5dff6686..00000000000 --- a/packages/@webex/plugin-cc/src/services/TaskControl/index.ts +++ /dev/null @@ -1,111 +0,0 @@ -import EventEmitter from 'events'; -import {ICall, LINE_EVENTS, LocalMicrophoneStream, createMicrophoneStream} from '@webex/calling'; -import {getErrorDetails} from '../core/Utils'; -import WebCallingService from '../WebCallingService'; -import {LoginOption} from '../../types'; -import {CC_FILE} from '../../constants'; -import {CC_EVENTS} from '../config/types'; -import {WebSocketManager} from '../core/WebSocket/WebSocketManager'; -import routingContact from './contact'; -import {AgentContact, TASK_EVENTS, TaskResponse} from './types'; - -export default class TaskControl extends EventEmitter { - private contact: ReturnType; - private call: ICall; - private localAudioStream: LocalMicrophoneStream; - private webCallingService: WebCallingService; - private webSocketManager: WebSocketManager; - private task: AgentContact; - - constructor( - contact: ReturnType, - webSocketManager: WebSocketManager, - webCallingService: WebCallingService - ) { - super(); - this.contact = contact; - this.webCallingService = webCallingService; - this.webSocketManager = webSocketManager; - this.registerTaskListeners(); - this.registerIncomingCallEvent(); - } - - private registerIncomingCallEvent() { - this.webCallingService.on(LINE_EVENTS.INCOMING_CALL, (call) => { - if (this.task) { - this.emit(TASK_EVENTS.TASK_INCOMING, this.task); - } - this.call = call; - }); - } - - private registerTaskListeners() { - // TODO: This event reception approach will be changed once state machine is implemented - this.webSocketManager.on('message', (event) => { - const payload = JSON.parse(event); - switch (payload.data.type) { - case CC_EVENTS.AGENT_CONTACT_RESERVED: - this.task = payload.data; - if (this.webCallingService.loginOption !== LoginOption.BROWSER) { - this.emit(TASK_EVENTS.TASK_INCOMING, payload.data); - } else if (this.call) { - this.emit(TASK_EVENTS.TASK_INCOMING, payload.data); - } - break; - case CC_EVENTS.AGENT_CONTACT_ASSIGNED: - this.emit(TASK_EVENTS.TASK_ASSIGNED, payload.data); - break; - default: - break; - } - }); - } - - /** - * This is used for incoming task accept by agent. - * @param data - * @returns Promise - * @throws Error - * @example - * ```typescript - * webex.cc.task.accept(taskId).then(()=>{}).catch(()=>{}) - * ``` - */ - public async accept(taskId: string): Promise { - try { - if (this.webCallingService.loginOption === LoginOption.BROWSER) { - // @ts-ignore - this.localAudioStream = await createMicrophoneStream({audio: true}); - this.webCallingService.answerCall(this.localAudioStream, taskId); - - return Promise.resolve(); // TODO: Update this with sending the task object received in AgentContactAssigned - } - - // TODO: Invoke the accept API from services layer. This is going to be used in Outbound Dialer scenario - return this.contact.accept({interactionId: taskId}); - } catch (error) { - throw getErrorDetails(error, 'accept', CC_FILE); - } - } - - /** - * This is used for the incoming task decline by agent. - * @param data - * @returns Promise - * @throws Error - * * ```typescript - * webex.cc.task.decline(taskId).then(()=>{}).catch(()=>{}) - * ``` - */ - public async decline(taskId: string): Promise { - try { - this.webCallingService.declinecall(taskId); - - return Promise.resolve(); - } catch (error) { - throw getErrorDetails(error, 'decline', CC_FILE); - } - } - - // TODO: Hold/resume, recording pause/resume, consult and transfer public methods to be implemented here -} diff --git a/packages/@webex/plugin-cc/src/services/WebSocket/config.ts b/packages/@webex/plugin-cc/src/services/WebSocket/config.ts deleted file mode 100644 index 12fde7a5a18..00000000000 --- a/packages/@webex/plugin-cc/src/services/WebSocket/config.ts +++ /dev/null @@ -1,47 +0,0 @@ -const webSocketConfig = { - /** - * Milliseconds between pings sent up the socket - * @type {number} - */ - pingInterval: process.env.MERCURY_PING_INTERVAL || 15000, - /** - * Milliseconds to wait for a pong before declaring the connection dead - * @type {number} - */ - pongTimeout: process.env.MERCURY_PONG_TIMEOUT || 14000, - /** - * Maximum milliseconds between connection attempts - * @type {Number} - */ - backoffTimeMax: process.env.MERCURY_BACKOFF_TIME_MAX || 32000, - /** - * Initial milliseconds between connection attempts - * @type {Number} - */ - backoffTimeReset: process.env.MERCURY_BACKOFF_TIME_RESET || 1000, - /** - * Milliseconds to wait for a close frame before declaring the socket dead and - * discarding it - * @type {[type]} - */ - forceCloseDelay: process.env.MERCURY_FORCE_CLOSE_DELAY || 2000, - /** - * When logging out, use default reason which can trigger a reconnect, - * or set to something else, like `done (permanent)` to prevent reconnect - * @type {String} - */ - beforeLogoutOptionsCloseReason: process.env.MERCURY_LOGOUT_REASON || 'done (forced)', - - /** - * Whether or not to authorize the websocket connection with the user's token - * - */ - authorizationRequired: false, - /** - * Whether or not to acknowledge the messenges received from the websocket - * - */ - acknowledgementRequired: false, -}; - -export default webSocketConfig; diff --git a/packages/@webex/plugin-cc/src/services/WebSocket/index.ts b/packages/@webex/plugin-cc/src/services/WebSocket/index.ts deleted file mode 100644 index 4392bf24b4e..00000000000 --- a/packages/@webex/plugin-cc/src/services/WebSocket/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -import Mercury from '@webex/internal-plugin-mercury'; -import {WebSocketEvent} from '../config/types'; -import webSocketConfig from './config'; -import IWebSocket from './types'; - -class WebSocket extends (Mercury as any) implements IWebSocket { - /** - * @instance - * @memberof WebSocket - * @private - * @type {string} - */ - private webSocketUrl: string; - - config = webSocketConfig; // overriding the config of Mercury with CC config - - constructor(options = {}) { - super(options); - Mercury.prototype.initialize(this, options); - } - - on(event: string, callback: (event: WebSocketEvent) => void): void { - super.on(event, callback); - } - - off(event: string, callback: (event: WebSocketEvent) => void): void { - super.off(event, callback); - } - - /** - * Subscribe and connect to the websocket - * @param {object} params - * @param {string} params.datachannelUrl - * @param {SubscribeRequest} params.body - * @returns {Promise} - */ - connectWebSocket(options: {webSocketUrl: string}): void { - const {webSocketUrl} = options; - this.webSocketUrl = webSocketUrl; - this.connect(webSocketUrl); - } - - /** - * Tells if WebSocket socket is connected - * @returns {boolean} connected - */ - isConnected(): boolean { - return this.connected; - } - - /** - * Get data channel URL for the connection - * @returns {string} data channel Url - */ - getWebSocketUrl(): string | undefined { - return this.webSocketUrl; - } - - /** - * Disconnects websocket connection - * @returns {Promise} - */ - disconnectWebSocket(): Promise { - return this.disconnect().then(() => { - this.webSocketUrl = undefined; - }); - } -} - -export default WebSocket; diff --git a/packages/@webex/plugin-cc/src/services/WebSocket/types.ts b/packages/@webex/plugin-cc/src/services/WebSocket/types.ts deleted file mode 100644 index 51ae926d9d9..00000000000 --- a/packages/@webex/plugin-cc/src/services/WebSocket/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {WebSocketEvent} from '../config/types'; - -// ts doc -/** - * Interface for WebSocket - */ -interface IWebSocket { - /** - * Subscribe to the WebSocket events - * @param {string} event - * @param {function} callback - * @returns {void} - */ - on(event: string, callback: (event: WebSocketEvent) => void): void; - /** - * Unsubscribe from the WebSocket events - * @param {string} event - * @param {function} callback - * @returns {void} - */ - off(event: string, callback: (event: WebSocketEvent) => void): void; - /** - * Subscribe and connect to the WebSocket - * @param {object} options - * @returns {void} - */ - connectWebSocket(options: {webSocketUrl: string}): void; - /** - * Check if the WebSocket connection is connected - * @returns {boolean} - */ - isConnected(): boolean; - /** - * Disconnect the WebSocket connection - * @returns {Promise} - */ - disconnectWebSocket(): Promise; - /** - * Get data channel URL for the connection - * @returns {string} data channel Url - */ - getWebSocketUrl(): string | undefined; -} - -export default IWebSocket; diff --git a/packages/@webex/plugin-cc/src/services/core/WebSocket/connection-service.ts b/packages/@webex/plugin-cc/src/services/core/WebSocket/connection-service.ts index f222d81071e..930e4307a0a 100644 --- a/packages/@webex/plugin-cc/src/services/core/WebSocket/connection-service.ts +++ b/packages/@webex/plugin-cc/src/services/core/WebSocket/connection-service.ts @@ -1,5 +1,5 @@ import {EventEmitter} from 'events'; -import {WebSocketManager} from './WebSocketManager'; +import {WebSocketManager} from './WebsocketManager'; import LoggerProxy from '../../../logger-proxy'; import {ConnectionServiceOptions, ConnectionLostDetails, ConnectionProp} from './types'; import { diff --git a/packages/@webex/plugin-cc/src/services/core/WebSocket/types.ts b/packages/@webex/plugin-cc/src/services/core/WebSocket/types.ts index 653c19e05cf..18a73c33438 100644 --- a/packages/@webex/plugin-cc/src/services/core/WebSocket/types.ts +++ b/packages/@webex/plugin-cc/src/services/core/WebSocket/types.ts @@ -1,5 +1,5 @@ import {SubscribeRequest} from '../../../types'; -import {WebSocketManager} from './WebSocketManager'; +import {WebSocketManager} from './WebsocketManager'; export type ConnectionServiceOptions = { webSocketManager: WebSocketManager; diff --git a/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts b/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts index 1ff1be9f374..76bf780fb52 100644 --- a/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts +++ b/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts @@ -1,12 +1,13 @@ import {Msg} from './GlobalTypes'; import * as Err from './Err'; import {HTTP_METHODS, WebexRequestPayload} from '../../types'; -import HttpRequest from './HttpRequest'; + import LoggerProxy from '../../logger-proxy'; import {CbRes, Conf, ConfEmpty, Pending, Req, Res, ResEmpty} from './types'; import {TIMEOUT_REQ} from './constants'; import {AQM_REQS_FILE} from '../../constants'; -import {WebSocketManager} from './WebSocket/WebSocketManager'; +import HttpRequest from './HttpRequest'; +import {WebSocketManager} from './websocket/WebSocketManager'; export default class AqmReqs { private pendingRequests: Record = {}; diff --git a/packages/@webex/plugin-cc/src/services/index.ts b/packages/@webex/plugin-cc/src/services/index.ts index ba8665f2896..1836d903fc7 100644 --- a/packages/@webex/plugin-cc/src/services/index.ts +++ b/packages/@webex/plugin-cc/src/services/index.ts @@ -1,9 +1,9 @@ import routingAgent from './agent'; -import routingContact from './TaskControl/contact'; +import routingContact from './task/contact'; import AgentConfigService from './config'; import AqmReqs from './core/aqm-reqs'; -import {WebSocketManager} from './core/WebSocket/WebSocketManager'; -import {ConnectionService} from './core/WebSocket/connection-service'; +import {WebSocketManager} from './core/websocket/WebSocketManager'; +import {ConnectionService} from './core/websocket/connection-service'; import {WebexSDK, SubscribeRequest} from '../types'; export default class Services { diff --git a/packages/@webex/plugin-cc/src/services/task/TaskManager.ts b/packages/@webex/plugin-cc/src/services/task/TaskManager.ts new file mode 100644 index 00000000000..f3069f80b9c --- /dev/null +++ b/packages/@webex/plugin-cc/src/services/task/TaskManager.ts @@ -0,0 +1,101 @@ +import EventEmitter from 'events'; +import {ICall, LINE_EVENTS} from '@webex/calling'; +import {WebSocketManager} from '../core/websocket/WebSocketManager'; +import routingContact from './contact'; +import WebCallingService from '../WebCallingService'; +import {ITask, TASK_EVENTS, TaskId} from './types'; +import {CC_EVENTS} from '../config/types'; +import {LoginOption} from '../../types'; +import Task from '.'; + +export default class TaskManager extends EventEmitter { + private call: ICall; + private contact: ReturnType; + private taskCollection: Record; + private webCallingService: WebCallingService; + private webSocketManager: WebSocketManager; + private static taskManager; + public task: ITask; + + /** + * @param contact - Routing Contact layer. Talks to AQMReq layer to convert events to promises + * @param webCallingService - Webrtc Service Layer + * @param webSocketManager - Websocket Manager to maintain websocket connection and keepalives + */ + constructor( + contact: ReturnType, + webCallingService: WebCallingService, + webSocketManager: WebSocketManager + ) { + super(); + this.contact = contact; + this.taskCollection = {}; + this.webCallingService = webCallingService; + this.webSocketManager = webSocketManager; + this.registerTaskListeners(); + this.registerIncomingCallEvent(); + } + + private registerIncomingCallEvent() { + this.webCallingService.on(LINE_EVENTS.INCOMING_CALL, (call) => { + if (this.task) { + this.emit(TASK_EVENTS.TASK_INCOMING, this.task); + } + this.call = call; + }); + } + + private registerTaskListeners() { + this.webSocketManager.on('message', (event) => { + const payload = JSON.parse(event); + if (!event.keepalive) { + switch (payload.data.type) { + case CC_EVENTS.AGENT_CONTACT_RESERVED: + this.task = new Task(this.contact, this.webCallingService, payload.data); + + this.taskCollection[payload.data.interactionId] = this.task; + if (this.webCallingService.loginOption !== LoginOption.BROWSER) { + this.emit(TASK_EVENTS.TASK_INCOMING, this.task); + } else if (this.call) { + this.emit(TASK_EVENTS.TASK_INCOMING, this.task); + } + break; + case CC_EVENTS.AGENT_CONTACT_ASSIGNED: + this.emit(TASK_EVENTS.TASK_ASSIGNED, payload.data); + break; + default: + break; + } + } + }); + } + + /** + * @param taskId - string. + */ + public getTask = (taskId: string) => { + return this.taskCollection[taskId]; + }; + + /** + * + */ + public getActiveTasks = (): Record => { + return this.taskCollection; + }; + + /** + * webSocketManager - WebSocketManager + */ + public static getTaskManager = ( + contact: ReturnType, + webCallingService: WebCallingService, + webSocketManager: WebSocketManager + ): TaskManager => { + if (!this.taskManager) { + this.taskManager = new TaskManager(contact, webCallingService, webSocketManager); + } + + return this.taskManager; + }; +} diff --git a/packages/@webex/plugin-cc/src/services/TaskControl/constants.ts b/packages/@webex/plugin-cc/src/services/task/constants.ts similarity index 86% rename from packages/@webex/plugin-cc/src/services/TaskControl/constants.ts rename to packages/@webex/plugin-cc/src/services/task/constants.ts index ed0f7e2931b..2d0dede27de 100644 --- a/packages/@webex/plugin-cc/src/services/TaskControl/constants.ts +++ b/packages/@webex/plugin-cc/src/services/task/constants.ts @@ -11,3 +11,5 @@ export const PAUSE = '/record/pause'; export const RESUME = 'record/resume'; export const WRAPUP = '/wrapup'; export const END = '/end'; +export const TASK_MANAGER_FILE = 'taskManager'; +export const TASK_FILE = 'task'; diff --git a/packages/@webex/plugin-cc/src/services/TaskControl/contact.ts b/packages/@webex/plugin-cc/src/services/task/contact.ts similarity index 100% rename from packages/@webex/plugin-cc/src/services/TaskControl/contact.ts rename to packages/@webex/plugin-cc/src/services/task/contact.ts index 499e8f4a0b0..1966c0341a0 100644 --- a/packages/@webex/plugin-cc/src/services/TaskControl/contact.ts +++ b/packages/@webex/plugin-cc/src/services/task/contact.ts @@ -16,8 +16,8 @@ import { UNHOLD, WRAPUP, } from './constants'; -import {DESTINATION_TYPE} from './types'; import * as Contact from './types'; +import {DESTINATION_TYPE} from './types'; export default function routingContact(aqm: AqmReqs) { return { 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..84ed20662d5 --- /dev/null +++ b/packages/@webex/plugin-cc/src/services/task/index.ts @@ -0,0 +1,83 @@ +import {LocalMicrophoneStream} from '@webex/calling'; +import {getErrorDetails} from '../core/Utils'; +import {LoginOption} from '../../types'; +import {CC_FILE} from '../../constants'; +import routingContact from './contact'; +import {ITask, TaskId, TaskResponse, TaskData} from './types'; +import WebCallingService from '../WebCallingService'; + +export default class Task implements ITask { + private contact: ReturnType; + private localAudioStream: LocalMicrophoneStream; + private webCallingService: WebCallingService; + public data: TaskData; + + constructor( + contact: ReturnType, + webCallingService: WebCallingService, + data: TaskData + ) { + this.contact = contact; + this.data = data; + this.webCallingService = webCallingService; + } + + /** + * This is used for incoming task accept by agent. + * + * @param taskId - Unique Id to identify each task + * + * @returns Promise + * @throws Error + * @example + * ```typescript + * task.accept(taskId).then(()=>{}).catch(()=>{}) + * ``` + */ + public async accept(taskId: TaskId): Promise { + try { + if (this.webCallingService.loginOption === LoginOption.BROWSER) { + const constraints = { + audio: true, + }; + + const localStream = await navigator.mediaDevices.getUserMedia(constraints); + const audioTrack = localStream.getAudioTracks()[0]; + this.localAudioStream = new LocalMicrophoneStream(new MediaStream([audioTrack])); + this.webCallingService.answerCall(this.localAudioStream, taskId); + + return Promise.resolve(); // TODO: Update this with sending the task object received in AgentContactAssigned + } + + // TODO: Invoke the accept API from services layer. This is going to be used in Outbound Dialer scenario + return this.contact.accept({interactionId: taskId}); + } catch (error) { + const {error: detailedError} = getErrorDetails(error, 'accept', CC_FILE); + throw detailedError; + } + } + + /** + * This is used for the incoming task decline by agent. + * + * @param taskId - Unique Id to identify each task + * + * @returns Promise + * @throws Error + * ```typescript + * task.decline(taskId).then(()=>{}).catch(()=>{}) + * ``` + */ + public async decline(taskId: TaskId): Promise { + try { + this.webCallingService.declinecall(taskId); + + return Promise.resolve(); + } catch (error) { + const {error: detailedError} = getErrorDetails(error, 'decline', CC_FILE); + throw detailedError; + } + } + + // TODO: Hold/resume, recording pause/resume, consult and transfer public methods to be implemented here +} diff --git a/packages/@webex/plugin-cc/src/services/TaskControl/types.ts b/packages/@webex/plugin-cc/src/services/task/types.ts similarity index 80% rename from packages/@webex/plugin-cc/src/services/TaskControl/types.ts rename to packages/@webex/plugin-cc/src/services/task/types.ts index 819bbd98f55..f3aa8232880 100644 --- a/packages/@webex/plugin-cc/src/services/TaskControl/types.ts +++ b/packages/@webex/plugin-cc/src/services/task/types.ts @@ -1,5 +1,7 @@ import {Msg} from '../core/GlobalTypes'; +export type TaskId = string; + type Enum> = T[keyof T]; export const DESTINATION_TYPE = { @@ -37,76 +39,7 @@ export const TASK_EVENTS = { export type TASK_EVENTS = Enum; -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 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 = { +type Interaction = { isFcManaged: boolean; isTerminated: boolean; mediaType: MEDIA_CHANNEL; @@ -206,6 +139,110 @@ export type Interaction = { >; }; +export type TaskData = { + 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; + id?: string; // unique id in monitoring offered event + isWebCallMute?: boolean; + reservationInteractionId?: 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 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 HoldResumePayload = { mediaResourceId: string; }; @@ -263,3 +300,31 @@ export type ContactCleanupData = { }; export type TaskResponse = AgentContact | Error | void; + +export interface ITask { + /** + * Event data received in the CC events + */ + data: TaskData; + /** + * Answers/accepts the incoming task + * + * @param taskId - Unique Task Identifier + * @example + * ``` + * task.accept(taskId); + * ``` + */ + accept(taskId: TaskId): Promise; + /** + * Decline the incoming task for Browser Login + * + * @param taskId - Unique Task Identifier + * @example + * ``` + * task.decline(taskId); + * ``` + */ + decline(taskId: TaskId): Promise; + // TODO: Add the remianing public methods +} diff --git a/packages/@webex/plugin-cc/src/types.ts b/packages/@webex/plugin-cc/src/types.ts index a80e2d5c365..38ab7e9369f 100644 --- a/packages/@webex/plugin-cc/src/types.ts +++ b/packages/@webex/plugin-cc/src/types.ts @@ -1,6 +1,6 @@ import {CallingClientConfig} from '@webex/calling'; import * as Agent from './services/agent/types'; -import * as Contact from './services/TaskControl/types'; +import * as Contact from './services/task/types'; import {Profile} from './services/config/types'; type Enum> = T[keyof T]; diff --git a/packages/@webex/plugin-cc/test/unit/spec/cc.ts b/packages/@webex/plugin-cc/test/unit/spec/cc.ts index ac316b214e1..d595703ffc2 100644 --- a/packages/@webex/plugin-cc/test/unit/spec/cc.ts +++ b/packages/@webex/plugin-cc/test/unit/spec/cc.ts @@ -35,7 +35,7 @@ jest.mock('../../../src/services/config'); jest.mock('../../../src/services/core/WebSocket/WebSocketManager'); jest.mock('../../../src/services/core/WebSocket/connection-service'); jest.mock('../../../src/services/WebCallingService'); -jest.mock('../../../src/services/TaskControl'); +jest.mock('../../../src/services/task/TaskManager'); global.URL.createObjectURL = jest.fn(() => 'blob:http://localhost:3000/12345'); @@ -45,7 +45,7 @@ describe('webex.cc', () => { let mockContact; beforeEach(() => { - webex = new MockWebex({ + webex = MockWebex({ children: { cc: ContactCenter, }, diff --git a/packages/@webex/plugin-cc/test/unit/spec/services/Task/contact.ts b/packages/@webex/plugin-cc/test/unit/spec/services/Task/contact.ts index 0c78b990962..5e85fcb2d4e 100644 --- a/packages/@webex/plugin-cc/test/unit/spec/services/Task/contact.ts +++ b/packages/@webex/plugin-cc/test/unit/spec/services/Task/contact.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import {DESTINATION_TYPE} from "../../../../../src/services/TaskControl/types"; +import {DESTINATION_TYPE} from "../../../../../src/services/task/types"; import AqmReqs from "../../../../../src/services/core/aqm-reqs"; -import routingContact from "../../../../../src/services/TaskControl/contact"; +import routingContact from "../../../../../src/services/task/contact"; jest.mock('../../../../../src/services/core/Utils', () => ({ createErrDetailsObject: jest.fn(), diff --git a/packages/@webex/plugin-cc/test/unit/spec/services/Task/index.ts b/packages/@webex/plugin-cc/test/unit/spec/services/Task/index.ts index 4db88e5c2b2..f91c081a4ae 100644 --- a/packages/@webex/plugin-cc/test/unit/spec/services/Task/index.ts +++ b/packages/@webex/plugin-cc/test/unit/spec/services/Task/index.ts @@ -4,7 +4,7 @@ import {CC_EVENTS} from '../../../../../src/services/config/types'; import {LoginOption} from '../../../../../src/types'; import {getErrorDetails} from '../../../../../src/services/core/Utils'; import { CC_FILE } from '../../../../../src/constants'; -import TaskControl from '../../../../../src/services/TaskControl'; +import TaskControl from '../../../../../src/services/task'; jest.mock('@webex/calling', () => ({ LINE_EVENTS: { @@ -109,13 +109,13 @@ describe('Task', () => { expect(servicesMock.contact.accept).toHaveBeenCalledWith({ interactionId: 'task-id' }); }); - // it('should handle errors in accept method', async () => { - // const error = new Error('Test Error'); - // createMicrophoneStream.mockRejectedValue(error); + it('should handle errors in accept method', async () => { + const error = new Error('Test Error'); + createMicrophoneStream.mockRejectedValue(error); - // expect(await task.decline('task-id')).toThrow(error); - // expect(getErrorDetails).toHaveBeenCalledWith(error, 'accept', CC_FILE); - // }); + expect(await task.decline('task-id')).rejects.toThrow(error); + expect(getErrorDetails).toHaveBeenCalledWith(error, 'accept', CC_FILE); + }); it('should decline call using webCallingService', async () => { await task.decline('task-id'); @@ -123,12 +123,12 @@ describe('Task', () => { expect(webCallingServiceMock.declinecall).toHaveBeenCalledWith('task-id'); }); - // it('should handle errors in decline method', async () => { - // const error = new Error('Test Error'); - // webCallingServiceMock.declinecall.mockImplementation(() => { throw error; }); + it('should handle errors in decline method', async () => { + const error = new Error('Test Error'); + webCallingServiceMock.declinecall.mockImplementation(() => { throw error; }); - // await task.decline('task-id'); - // expect(webCallingServiceMock.declinecall).toHaveBeenCalledWith('task-id'); - // expect(getErrorDetails).toHaveBeenCalledWith(error, 'decline', CC_FILE); - // }); + await task.decline('task-id'); + expect(webCallingServiceMock.declinecall).toHaveBeenCalledWith('task-id'); + expect(getErrorDetails).toHaveBeenCalledWith(error, 'decline', CC_FILE); + }); }); \ No newline at end of file