diff --git a/docs/samples/contact-center/app.js b/docs/samples/contact-center/app.js index b71713ced9c..056b1aae6cb 100644 --- a/docs/samples/contact-center/app.js +++ b/docs/samples/contact-center/app.js @@ -32,6 +32,7 @@ const dialNumber = document.querySelector('#dialNumber'); const registerStatus = document.querySelector('#ws-connection-status'); const idleCodesDropdown = document.querySelector('#idleCodesDropdown') const setAgentStatusButton = document.querySelector('#setAgentStatus'); +const logoutAgentElm = document.querySelector('#logoutAgent'); // Store and Grab `access-token` from sessionStorage if (sessionStorage.getItem('date') > new Date().getTime()) { @@ -103,7 +104,7 @@ function initWebex(e) { console.log('Authentication#initWebex() :: Webex Ready'); authStatusElm.innerText = 'Saved access token!'; - registerStatus.innerHTML = 'Not Registered'; + registerStatus.innerHTML = 'Not Subscribed'; registerBtn.disabled = false; }); @@ -115,7 +116,7 @@ credentialsFormElm.addEventListener('submit', initWebex); function register() { webex.cc.register(true).then((agentProfile) => { - registerStatus.innerHTML = 'Registered'; + registerStatus.innerHTML = 'Subscribed'; console.log('Event subscription successful: ', agentProfile); teamsDropdown.innerHTML = ''; // Clear previously selected option on teamsDropdown const listTeams = agentProfile.teams; @@ -171,6 +172,17 @@ async function handleAgentLogin(e) { function doAgentLogin() { webex.cc.stationLogin({teamId: teamsDropdown.value, loginOption: agentDeviceType, dialNumber: dialNumber.value}).then((response) => { console.log('Agent Logged in successfully', response); + logoutAgentElm.classList.remove('hidden'); + // Re-Login Agent after 5 seconds for testing purpose + setTimeout(async () => { + try { + const response = await webex.cc.stationReLogin(); + + console.log('Agent Re-Login successful', response); + } catch (error) { + console.log('Agent Re-Login failed', error); + } + }, 5000); } ).catch((error) => { console.error('Agent Login failed', error); @@ -193,6 +205,20 @@ function setAgentStatus() { }); } + +function logoutAgent() { + webex.cc.stationLogout({logoutReason: 'logout'}).then((response) => { + console.log('Agent logged out successfully', response); + + setTimeout(() => { + logoutAgentElm.classList.add('hidden'); + }, 1000); + } + ).catch((error) => { + console.log('Agent logout failed', error); + }); +} + const allCollapsibleElements = document.querySelectorAll('.collapsible'); allCollapsibleElements.forEach((el) => { el.addEventListener('click', (event) => { diff --git a/docs/samples/contact-center/index.html b/docs/samples/contact-center/index.html index a26904070c6..f856b4c312c 100644 --- a/docs/samples/contact-center/index.html +++ b/docs/samples/contact-center/index.html @@ -76,7 +76,7 @@

-

Not Registered

+

Not Subscribed

@@ -88,7 +88,7 @@

- Agent Desktop Using Webex CC SDK + Agent Station Login/Logout

@@ -112,6 +112,7 @@

+
Agent status diff --git a/packages/@webex/plugin-cc/package.json b/packages/@webex/plugin-cc/package.json index 7e62f3bab77..f6daba0b555 100644 --- a/packages/@webex/plugin-cc/package.json +++ b/packages/@webex/plugin-cc/package.json @@ -13,7 +13,7 @@ }, "scripts": { "build:src": "webex-legacy-tools build -dest \"./dist\" -src \"./src\" -js -ts && yarn build", - "build": " yarn workspace @webex/calling run build:src && yarn run -T tsc --declaration true --declarationDir ./dist/types", + "build": "yarn run -T tsc --declaration true --declarationDir ./dist/types", "docs": "typedoc --emit none", "fix:lint": "eslint 'src/**/*.ts' --fix", "fix:prettier": "prettier \"src/**/*.ts\" --write", diff --git a/packages/@webex/plugin-cc/src/cc.ts b/packages/@webex/plugin-cc/src/cc.ts index 1558f014134..b2d59bd9068 100644 --- a/packages/@webex/plugin-cc/src/cc.ts +++ b/packages/@webex/plugin-cc/src/cc.ts @@ -1,22 +1,26 @@ import {WebexPlugin} from '@webex/webex-core'; import AgentConfig from './features/Agentconfig'; -import {IAgentProfile, SetStateResponse, StationLoginResponse} from './features/types'; +import {SetStateResponse} from './features/types'; import { CCPluginConfig, IContactCenter, WebexSDK, - SubscribeRequest, LoginOption, WelcomeEvent, + IAgentProfile, + AgentLogin, + StationLoginResponse, + StationLogoutResponse, + StationReLoginResponse, } from './types'; -import {READY, CC_FILE} from './constants'; +import {READY, CC_FILE, EMPTY_STRING} from './constants'; import HttpRequest from './services/core/HttpRequest'; -import WebCallingService from './WebCallingService'; -import {AgentLogin} from './services/config/types'; +import WebCallingService from './services/WebCallingService'; import {AGENT, WEB_RTC_PREFIX} from './services/constants'; import Services from './services'; import LoggerProxy from './logger-proxy'; -import {StateChange} from './services/agent/types'; +import {StateChange, Logout} from './services/agent/types'; +import {getErrorDetails} from './services/core/Utils'; export default class ContactCenter extends WebexPlugin implements IContactCenter { namespace = 'cc'; @@ -62,7 +66,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter } catch (error) { this.$webex.logger.error(`file: ${CC_FILE}: Error during register: ${error}`); - return Promise.reject(new Error('Error while performing register`', error)); + throw error; } } @@ -73,7 +77,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter * @private */ private async connectWebsocket() { - const connectionConfig: SubscribeRequest = { + const connectionConfig = { force: this.$config?.force ?? true, isKeepAliveEnabled: this.$config?.isKeepAliveEnabled ?? false, clientType: this.$config?.clientType ?? 'WebexCCSDK', @@ -106,7 +110,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter /** * This is used for agent login. * @param data - * @returns Promise + * @returns Promise * @throws Error */ public async stationLogin(data: AgentLogin): Promise { @@ -119,10 +123,11 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter isExtension: data.loginOption === LoginOption.EXTENSION, deviceId: this.getDeviceId(data.loginOption, data.dialNumber), roles: [AGENT], - teamName: '', - siteId: '', + // TODO: The public API should not have the following properties so filling them with empty values for now. If needed, we can add them in the future. + teamName: EMPTY_STRING, + siteId: EMPTY_STRING, usesOtherDN: false, - auxCodeId: '', + auxCodeId: EMPTY_STRING, }, }); @@ -133,9 +138,45 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter await loginResponse; return loginResponse; - } catch (error: any) { - this.$webex.logger.log(`file: ${CC_FILE}: Station Login FAILED: ${error.id}`); - throw new Error(error.details?.data?.reason ?? 'Error while performing station login'); + } catch (error) { + throw getErrorDetails(error, 'stationLogin'); + } + } + + /** This is used for agent logout. + * @param data + * @returns Promise + * @throws Error + */ + public async stationLogout(data: Logout): Promise { + try { + const logoutResponse = this.services.agent.logout({ + data, + }); + + await logoutResponse; + + if (this.webCallingService) { + this.webCallingService.deregisterWebCallingLine(); + } + + return logoutResponse; + } catch (error) { + throw getErrorDetails(error, 'stationLogout'); + } + } + + /* This is used for agent relogin. + * @returns Promise + * @throws Error + */ + public async stationReLogin(): Promise { + try { + const reLoginResponse = await this.services.agent.reload(); + + return reLoginResponse; + } catch (error) { + throw getErrorDetails(error, 'stationReLogin'); } } diff --git a/packages/@webex/plugin-cc/src/config.ts b/packages/@webex/plugin-cc/src/config.ts index 8eedc39a1cd..caaba9c18a5 100644 --- a/packages/@webex/plugin-cc/src/config.ts +++ b/packages/@webex/plugin-cc/src/config.ts @@ -16,6 +16,7 @@ export default { }, serviceData: { indicator: 'contactcenter', + // TODO: This should be dynamic based on the environment domain: 'rtw.prod-us1.rtmsprod.net', }, }, diff --git a/packages/@webex/plugin-cc/src/constants.ts b/packages/@webex/plugin-cc/src/constants.ts index 8e1eee65da0..9eb57f14eac 100644 --- a/packages/@webex/plugin-cc/src/constants.ts +++ b/packages/@webex/plugin-cc/src/constants.ts @@ -2,3 +2,4 @@ export const EVENT = 'event'; export const READY = 'ready'; export const CC_FILE = 'cc'; export const TIMEOUT_DURATION = 20000; // 20 seconds timeout duration for webrtc registration +export const EMPTY_STRING = ''; diff --git a/packages/@webex/plugin-cc/src/features/Agentconfig.ts b/packages/@webex/plugin-cc/src/features/Agentconfig.ts index 011b4b5ca13..89641951c63 100644 --- a/packages/@webex/plugin-cc/src/features/Agentconfig.ts +++ b/packages/@webex/plugin-cc/src/features/Agentconfig.ts @@ -1,7 +1,6 @@ -import {IAgentProfile, WORK_TYPE_CODE} from './types'; +import {WORK_TYPE_CODE} from './types'; import AgentConfigService from '../services/config'; -import {Team, AuxCode} from '../services/config/types'; -import {WebexSDK} from '../types'; +import {WebexSDK, IAgentProfile, Team, AuxCode} from '../types'; import { AGENT_STATE_AVAILABLE, AGENT_STATE_AVAILABLE_DESCRIPTION, diff --git a/packages/@webex/plugin-cc/src/features/types.ts b/packages/@webex/plugin-cc/src/features/types.ts index f619639a527..8ff0528c982 100644 --- a/packages/@webex/plugin-cc/src/features/types.ts +++ b/packages/@webex/plugin-cc/src/features/types.ts @@ -1,5 +1,4 @@ import {WebexSDK} from '../types'; -import {AuxCode, Team} from '../services/config/types'; import * as Agent from '../services/agent/types'; type Enum> = T[keyof T]; @@ -36,57 +35,6 @@ export type AgentConfigRequest = { orgId: string; }; -/** - * Represents the response from AgentConfig. - * - * @public - */ -export type IAgentProfile = { - /** - * The id of the agent. - */ - - agentId: string; - - /** - * The name of the agent. - */ - agentName: string; - - /** - * Identifier for a Desktop Profile. - */ - agentProfileId: string; - - /** - * The email address of the agent. - */ - - agentMailId: string; - - /** - * Represents list of teams of an agent. - */ - teams: Team[]; - - /** - * Represents the voice options of an agent. - */ - - loginVoiceOptions: string[]; - - /** - * Represents the Idle codes list that the agents can select in Agent Desktop.t. - */ - - idleCodes: AuxCode[]; - - /** - * Represents the wrap-up codes list that the agents can select when they wrap up a contact. - */ - wrapUpCodes: AuxCode[]; -}; - /** * Represents the response from setAgentStatus. * diff --git a/packages/@webex/plugin-cc/src/WebCallingService.ts b/packages/@webex/plugin-cc/src/services/WebCallingService.ts similarity index 85% rename from packages/@webex/plugin-cc/src/WebCallingService.ts rename to packages/@webex/plugin-cc/src/services/WebCallingService.ts index 98bcfb28d17..27154131ac7 100644 --- a/packages/@webex/plugin-cc/src/WebCallingService.ts +++ b/packages/@webex/plugin-cc/src/services/WebCallingService.ts @@ -1,7 +1,13 @@ -import {createClient, ICall, ICallingClient, ILine, LINE_EVENTS} from '@webex/calling'; -import {CallingClientConfig} from '@webex/calling/dist/types/CallingClient/types'; -import {WebexSDK} from './types'; -import {TIMEOUT_DURATION} from './constants'; +import { + createClient, + ICall, + ICallingClient, + ILine, + LINE_EVENTS, + CallingClientConfig, +} from '@webex/calling'; +import {WebexSDK} from '../types'; +import {TIMEOUT_DURATION} from '../constants'; export default class WebCallingService { private callingClient: ICallingClient; @@ -52,6 +58,6 @@ export default class WebCallingService { } public async deregisterWebCallingLine() { - this.line.deregister(); + this.line?.deregister(); } } diff --git a/packages/@webex/plugin-cc/src/services/agent/index.ts b/packages/@webex/plugin-cc/src/services/agent/index.ts index 6add776584b..b62cb249201 100644 --- a/packages/@webex/plugin-cc/src/services/agent/index.ts +++ b/packages/@webex/plugin-cc/src/services/agent/index.ts @@ -1,9 +1,10 @@ import * as Err from '../core/Err'; -import {Failure, Msg} from '../core/GlobalTypes'; -import {createErrDetailsObject as err, getRoutingHost} from '../core/Utils'; +import {createErrDetailsObject as err} from '../core/Utils'; import * as Agent from './types'; -import {AqmReqs} from '../core/aqm-reqs'; +import AqmReqs from '../core/aqm-reqs'; import {HTTP_METHODS} from '../../types'; +import {WCC_API_GATEWAY} from '../constants'; +import {CC_EVENTS} from '../config/types'; /* * routingAgent @@ -14,48 +15,48 @@ import {HTTP_METHODS} from '../../types'; export default function routingAgent(routing: AqmReqs) { return { reload: routing.reqEmpty(() => ({ - host: getRoutingHost(), + host: WCC_API_GATEWAY, url: '/v1/agents/reload', data: {}, err, notifSuccess: { bind: { - type: 'AgentReloginSuccess', - data: {type: 'AgentReloginSuccess'}, + type: CC_EVENTS.AGENT_RELOGIN_SUCCESS, + data: {type: CC_EVENTS.AGENT_RELOGIN_SUCCESS}, }, msg: {} as Agent.ReloginSuccess, }, notifFail: { bind: { - type: 'AgentReloginFailed', - data: {type: 'AgentReloginFailed'}, + type: CC_EVENTS.AGENT_RELOGIN_FAILED, + data: {type: CC_EVENTS.AGENT_RELOGIN_FAILED}, }, errId: 'Service.aqm.agent.reload', }, })), logout: routing.req((p: {data: Agent.Logout}) => ({ url: '/v1/agents/logout', - host: getRoutingHost(), + host: WCC_API_GATEWAY, data: p.data, err, notifSuccess: { bind: { - type: 'Logout', - data: {type: 'AgentLogoutSuccess'}, + type: CC_EVENTS.AGENT_LOGOUT, + data: {type: CC_EVENTS.AGENT_LOGOUT_SUCCESS}, }, msg: {} as Agent.LogoutSuccess, }, notifFail: { bind: { - type: 'Logout', - data: {type: 'AgentLogoutFailed'}, + type: CC_EVENTS.AGENT_LOGOUT, + data: {type: CC_EVENTS.AGENT_LOGOUT_FAILED}, }, errId: 'Service.aqm.agent.logout', }, })), stationLogin: routing.req((p: {data: Agent.UserStationLogin}) => ({ url: '/v1/agents/login', - host: getRoutingHost(), + host: WCC_API_GATEWAY, data: p.data, err: /* istanbul ignore next */ (e: any) => new Err.Details('Service.aqm.agent.stationLogin', { @@ -65,104 +66,39 @@ export default function routingAgent(routing: AqmReqs) { }), notifSuccess: { bind: { - type: 'StationLogin', - data: {type: 'AgentStationLoginSuccess'}, + type: CC_EVENTS.AGENT_STATION_LOGIN, + data: {type: CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS}, }, msg: {} as Agent.StationLoginSuccess, }, notifFail: { bind: { - type: 'StationLogin', - data: {type: 'AgentStationLoginFailed'}, + type: CC_EVENTS.AGENT_STATION_LOGIN, + data: {type: CC_EVENTS.AGENT_STATION_LOGIN_FAILED}, }, errId: 'Service.aqm.agent.stationLoginFailed', }, })), stateChange: routing.req((p: {data: Agent.StateChange}) => ({ url: '/v1/agents/session/state', - host: getRoutingHost(), + host: WCC_API_GATEWAY, data: {...p.data, auxCodeId: p.data.auxCodeIdArray}, err, method: HTTP_METHODS.PUT, notifSuccess: { bind: { - type: 'AgentStateChange', - data: {type: 'AgentStateChangeSuccess'}, + type: CC_EVENTS.AGENT_STATE_CHANGE, + data: {type: CC_EVENTS.AGENT_STATE_CHANGE_SUCCESS}, }, msg: {} as Agent.StateChangeSuccess, }, notifFail: { bind: { - type: 'AgentStateChange', - data: {type: 'AgentStateChangeFailed'}, + type: CC_EVENTS.AGENT_STATE_CHANGE, + data: {type: CC_EVENTS.AGENT_STATE_CHANGE_FAILED}, }, errId: 'Service.aqm.agent.stateChange', }, })), - eMockOutdialAniList: routing.evt({ - bind: { - type: 'mockOutdialAniList', - }, - msg: {} as Agent.OutdialAniListSuccess, - }), - - eAgentDNRegistered: routing.evt({ - bind: { - type: 'RoutingMessage', - data: {type: 'AgentDNRegistered'}, - }, - msg: {} as Agent.DNRegistered, - }), - - eAgentDNRegisterFailure: routing.evt({ - bind: { - type: 'RoutingMessage', - data: {type: 'AgentDNRegisterFailure'}, - }, - msg: {} as Failure, - }), - - eAgentMultiLogin: routing.evt({ - bind: { - type: 'AGENT_MULTI_LOGIN', - data: {type: 'AgentMultiLoginCloseSession'}, - }, - msg: {} as Msg<{ - agentId: string; - reason: string; - type: 'AgentMultiLoginCloseSession'; - agentSessionId: string; - }>, - }), - - // jsapi required events - eAgentReloginSuccess: routing.evt({ - bind: { - type: 'AgentReloginSuccess', - data: {type: 'AgentReloginSuccess'}, - }, - msg: {} as Agent.ReloginSuccess, - }), - eAgentStationLoginSuccess: routing.evt({ - bind: { - type: 'StationLogin', - data: {type: 'AgentStationLoginSuccess'}, - }, - msg: {} as Agent.StationLoginSuccess, - }), - eAgentStateChangeSuccess: routing.evt({ - bind: { - type: 'AgentStateChange', - data: {type: 'AgentStateChangeSuccess'}, - }, - msg: {} as Agent.StateChangeSuccess, - }), - eAgentLogoutSuccess: routing.evt({ - bind: { - type: 'Logout', - data: {type: 'AgentLogoutSuccess'}, - }, - msg: {} as Agent.LogoutSuccess, - }), }; } diff --git a/packages/@webex/plugin-cc/src/services/agent/types.ts b/packages/@webex/plugin-cc/src/services/agent/types.ts index 7e7e5e2d548..99c7c029759 100644 --- a/packages/@webex/plugin-cc/src/services/agent/types.ts +++ b/packages/@webex/plugin-cc/src/services/agent/types.ts @@ -89,22 +89,6 @@ export type DNRegistered = Msg<{ type: 'AgentDNRegistered'; }>; -export type OutdialAniListSuccess = Msg<{ - data: Record; -}>; - -export type OutdialAni = { - id: string; - name: string; -}; - -export type OutDialAniData = { - initialFetchCompleted: boolean; - data: OutdialAni[]; -}; - -// PAYLOAD - export type Logout = {logoutReason?: 'User requested logout' | 'Inactivity Logout'}; export type AgentState = 'Available' | 'Idle' | 'RONA' | string; @@ -132,24 +116,6 @@ export type UserStationLogin = { isEmergencyModalAlreadyDisplayed?: boolean; }; -export type AddressBooks = { - totalRecords?: number; - totalPages?: number; - page?: number; - speedDials: Address[]; -}; - -export type Address = { - desc: string; - dn: string; - phoneBookName?: string; -}; - -export type AddressBooksData = { - initialFetchCompleted: boolean; - data: Address[]; - errorObj: any; -}; export type LoginOption = 'AGENT_DN' | 'EXTENSION' | 'BROWSER'; -export type DeviceType = null | LoginOption | string; // cleanup this while removing FF: wxcc_webrtc. +export type DeviceType = LoginOption | string; diff --git a/packages/@webex/plugin-cc/src/services/config/index.ts b/packages/@webex/plugin-cc/src/services/config/index.ts index c883cc02bd1..192f29f87c4 100644 --- a/packages/@webex/plugin-cc/src/services/config/index.ts +++ b/packages/@webex/plugin-cc/src/services/config/index.ts @@ -1,5 +1,5 @@ -import {WebexSDK, HTTP_METHODS} from '../../types'; -import {DesktopProfileResponse, ListAuxCodesResponse, Team, AgentResponse} from './types'; +import {WebexSDK, HTTP_METHODS, Team} from '../../types'; +import {DesktopProfileResponse, ListAuxCodesResponse, AgentResponse} from './types'; import HttpRequest from '../core/HttpRequest'; import {WCC_API_GATEWAY} from '../constants'; diff --git a/packages/@webex/plugin-cc/src/services/config/types.ts b/packages/@webex/plugin-cc/src/services/config/types.ts index c259c0bcb18..235d56aa575 100644 --- a/packages/@webex/plugin-cc/src/services/config/types.ts +++ b/packages/@webex/plugin-cc/src/services/config/types.ts @@ -1,10 +1,22 @@ -import {LoginOption} from '../../types'; +import {AuxCode, WelcomeEvent} from '../../types'; +import * as Agent from '../agent/types'; type Enum> = T[keyof T]; // Define the CC_EVENTS object export const CC_EVENTS = { WELCOME: 'Welcome', + AGENT_RELOGIN_SUCCESS: 'AgentReloginSuccess', + AGENT_RELOGIN_FAILED: 'AgentReloginFailed', + AGENT_LOGOUT: 'Logout', + AGENT_LOGOUT_SUCCESS: 'AgentLogoutSuccess', + AGENT_LOGOUT_FAILED: 'AgentLogoutFailed', + AGENT_STATION_LOGIN: 'StationLogin', + AGENT_STATION_LOGIN_SUCCESS: 'AgentStationLoginSuccess', + AGENT_STATION_LOGIN_FAILED: 'AgentStationLoginFailed', + AGENT_STATE_CHANGE: 'AgentStateChange', + AGENT_STATE_CHANGE_SUCCESS: 'AgentStateChangeSuccess', + AGENT_STATE_CHANGE_FAILED: 'AgentStateChangeFailed', } as const; // Derive the type using the utility type @@ -12,9 +24,12 @@ export type CC_EVENTS = Enum; export type WebSocketEvent = { type: CC_EVENTS; - data: { - agentId: string; - }; + data: + | WelcomeEvent + | Agent.StationLoginSuccess + | Agent.LogoutSuccess + | Agent.ReloginSuccess + | Agent.StateChangeSuccess; }; /** @@ -88,31 +103,6 @@ export type DesktopProfileResponse = { idleCodes: string[]; }; -/** - * Represents the request to a AgentLogin - * - * @public - */ -export type AgentLogin = { - /** - * A dialNumber field contains the number to dial such as a route point or extension. - */ - - dialNumber?: string; - - /** - * The unique ID representing a team of users. - */ - - teamId: string; - - /** - * The loginOption field contains the type of login. - */ - - loginOption: LoginOption; -}; - export interface StateChange { state: string; auxCodeId: string; @@ -120,22 +110,6 @@ export interface StateChange { agentId?: string; } -export type UserStationLogin = { - dialNumber?: string | null; - dn?: string | null; - teamId: string | null; - teamName?: string | null; - roles?: Array; - siteId?: string; - usesOtherDN?: boolean; - skillProfileId?: string; - auxCodeId?: string; - isExtension?: boolean; - deviceType?: LoginOption; - deviceId: string | null; - isEmergencyModalAlreadyDisplayed?: boolean; -}; - export type SubscribeResponse = { statusCode: number; body: { @@ -145,66 +119,6 @@ export type SubscribeResponse = { message: string | null; }; -/** - * Represents the response from getListOfTeams method. - * - * @public - */ -export type Team = { - /** - * ID of the team. - */ - id: string; - - /** - * Name of the Team. - */ - name: string; -}; - -/** - * Represents AuxCode. - * @public - */ - -export type AuxCode = { - /** - * ID of the Auxiliary Code. - */ - id: string; - - /** - * Indicates whether the auxiliary code is active or not active. - */ - active: boolean; - - /** - * Indicates whether this is the default code (true) or not (false). - */ - defaultCode: boolean; - - /** - * Indicates whether this is the system default code (true) or not (false). - */ - isSystemCode: boolean; - - /** - * A short description indicating the context of the code. - */ - description: string; - - /** - * Name for the Auxiliary Code. - */ - name: string; - - /** - * Indicates the work type associated with this code.. - */ - - workTypeCode: string; -}; - /** * Represents the response from getListOfAuxCodes method. * diff --git a/packages/@webex/plugin-cc/src/services/core/Err.ts b/packages/@webex/plugin-cc/src/services/core/Err.ts index bc73ac3dc56..f194e4ad62f 100644 --- a/packages/@webex/plugin-cc/src/services/core/Err.ts +++ b/packages/@webex/plugin-cc/src/services/core/Err.ts @@ -1,7 +1,6 @@ import {WebexRequestPayload} from '../../types'; import {Failure} from './GlobalTypes'; -/* eslint-disable @typescript-eslint/no-namespace */ export type ErrDetails = {status: number; type: string; trackingId: string}; export type AgentErrorIds = @@ -10,15 +9,13 @@ export type AgentErrorIds = | {'Service.aqm.agent.stateChange': Failure} | {'Service.aqm.agent.reload': Failure} | {'Service.aqm.agent.logout': Failure} - | {'Service.aqm.agent.mockOutdialAniList': Failure} - | {'Service.reqs.generic.failure': {trackingId: string}} - | 'Service.aqm.agent.fetchAddressBooks'; + | {'Service.reqs.generic.failure': {trackingId: string}}; export type ReqError = | 'Service.aqm.reqs.GenericRequestError' | {'Service.aqm.reqs.Pending': {key: string; msg: string}} | {'Service.aqm.reqs.PendingEvent': {key: string}} - | {'Service.aqm.reqs.Timeout': {key: string; resAxios: WebexRequestPayload}} + | {'Service.aqm.reqs.Timeout': {key: string; response: WebexRequestPayload}} | {'Service.aqm.reqs.TimeoutEvent': {key: string}}; export interface Ids { @@ -26,6 +23,34 @@ export interface Ids { 'Service.aqm.reqs': ReqError; } +export type IdsGlobal = + | 'system' // to handle errors that was not created by 'new Err.WithId()' + | 'handle' + | 'fallback'; + +export type IdsSub = Ids[keyof Ids]; + +export type IdsMessage = IdsGlobal | keyof Ids | Exclude; + +export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( + k: infer I +) => void + ? I + : never; + +export type FlattenUnion = { + [K in keyof UnionToIntersection]: K extends keyof T + ? T[K] extends any[] + ? T[K] + : T[K] extends object + ? FlattenUnion + : T[K] + : UnionToIntersection[K]; +}; +export type IdsDetailsType = FlattenUnion>; + +export type IdsDetails = keyof IdsDetailsType; + export type Id = IdsMessage | IdsDetails; export class Message extends Error { @@ -69,31 +94,3 @@ export class Details extends Error { // Marker to distinct Err class from other errors private isErr = 'yes'; } - -// -------------------- - -export type IdsGlobal = - | 'system' // to handle errors that was not created by 'new Err.WithId()' - | 'handle' - | 'fallback'; - -export type IdsSub = Ids[keyof Ids]; - -export type IdsMessage = IdsGlobal | keyof Ids | Exclude; -export type IdsDetails = keyof IdsDetailsType; -export type IdsDetailsType = FlattenUnion>; - -export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( - k: infer I -) => void - ? I - : never; -export type FlattenUnion = { - [K in keyof UnionToIntersection]: K extends keyof T - ? T[K] extends any[] - ? T[K] - : T[K] extends object - ? FlattenUnion - : T[K] - : UnionToIntersection[K]; -}; diff --git a/packages/@webex/plugin-cc/src/services/core/HttpRequest.ts b/packages/@webex/plugin-cc/src/services/core/HttpRequest.ts index a9e9ce50e62..b11beed127e 100644 --- a/packages/@webex/plugin-cc/src/services/core/HttpRequest.ts +++ b/packages/@webex/plugin-cc/src/services/core/HttpRequest.ts @@ -6,6 +6,7 @@ import { IHttpResponse, WelcomeResponse, WelcomeEvent, + RequestBody, } from '../../types'; import IWebSocket from '../WebSocket/types'; import WebSocket from '../WebSocket'; @@ -85,68 +86,11 @@ class HttpRequest { }); } - /* This sends a request and waits for a websocket event - * It sends the request and then listens for the event type specified in the options - * If the event type is received, it resolves the promise - * If the event type is not received, it rejects the promise - */ - public async sendRequestWithEvent(options: { - service: string; - resource: string; - method: HTTP_METHODS; - payload: object; - eventType: string; - success: string[]; - failure: string[]; - }): Promise { - try { - const {service, resource, method, payload, eventType, success, failure} = options; - - // Send the service request - const response = await this.webex.request({ - service, - resource, - method, - body: payload, - }); - this.webex.logger.log(`Service request sent successfully: ${response}`); - - // Listen for the event - return new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { - this.webex.logger.error('Timeout waiting for event'); - this.eventHandlers.delete(eventType); - reject(new Error('Timeout waiting for event')); - }, WEBSOCKET_EVENT_TIMEOUT); - - // Store the event handler - this.eventHandlers.set(eventType, (data: any) => { - clearTimeout(timeoutId); - this.eventHandlers.delete(eventType); - if (success.includes(data.type)) { - resolve(data); - } else if (failure.includes(data.type)) { - const error = new Error(); - error.name = data.type; - error.message = data.reason; - reject(error); - } else { - // If event type is neither in success nor failure, handle it accordingly - reject(new Error(`Unexpected event type received: ${data.type}`)); - } - }); - }); - } catch (error) { - this.webex.logger.error(`Error sending service request: ${error}`); - throw error; - } - } - public async request(options: { service: string; resource: string; method: HTTP_METHODS; - body?: object; + body?: RequestBody; }): Promise { const {service, resource, method, body} = options; diff --git a/packages/@webex/plugin-cc/src/services/core/Signal.ts b/packages/@webex/plugin-cc/src/services/core/Signal.ts deleted file mode 100644 index 76bfe96d046..00000000000 --- a/packages/@webex/plugin-cc/src/services/core/Signal.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* eslint-disable @typescript-eslint/no-namespace */ -export namespace Signal { - class WithDataClass implements WithData { - private listeners: Listener[] = []; - private listenersOnce: Listener[] = []; - - listen = (listener: Listener) => { - this.listeners.push(listener); - - return {stopListen: () => this.stopListen(listener)}; - }; - - listenOnce = (listener: Listener) => { - this.listenersOnce.push(listener); - - return {stopListenOnce: () => this.stopListenOnce(listener)}; - }; - - stopListen = (listener: Listener) => { - const index = this.listeners.indexOf(listener, 0); - if (index > -1) { - this.listeners.splice(index, 1); - - return true; - } - - return false; - }; - - stopListenOnce = (listener: Listener) => { - const index = this.listenersOnce.indexOf(listener, 0); - if (index > -1) { - this.listenersOnce.splice(index, 1); - - return true; - } - - return false; - }; - - // concealed - stopListenAll = () => { - this.listeners = []; - this.listenersOnce = []; - }; - - // concealed - send = (data: T) => { - this.listeners.forEach((listener) => listener(data)); - this.listenersOnce.forEach((listener) => listener(data)); - this.listenersOnce = []; - }; - } - - class EmptyClass implements Empty { - private listeners: ListenerEmpty[] = []; - private listenersOnce: ListenerEmpty[] = []; - - listen = (listener: ListenerEmpty) => { - this.listeners.push(listener); - - return {stopListen: () => this.stopListen(listener)}; - }; - - listenOnce = (listener: ListenerEmpty) => { - this.listenersOnce.push(listener); - - return {stopListenOnce: () => this.stopListenOnce(listener)}; - }; - - stopListen = (listener: ListenerEmpty) => { - const index = this.listeners.indexOf(listener, 0); - if (index > -1) { - this.listeners.splice(index, 1); - - return true; - } - - return false; - }; - - stopListenOnce = (listener: ListenerEmpty) => { - const index = this.listenersOnce.indexOf(listener, 0); - if (index > -1) { - this.listenersOnce.splice(index, 1); - - return true; - } - - return false; - }; - - // concealed - stopListenAll = () => { - this.listeners = []; - this.listenersOnce = []; - }; - - // concealed - send = () => { - this.listeners.forEach((listener) => listener()); - this.listenersOnce.forEach((listener) => listener()); - this.listenersOnce = []; - }; - } - - type Listener = (data: T) => void; - type ListenerEmpty = () => void; - - export type Send = (data: T) => void; - export type SendEmpty = () => void; - - export interface WithData { - listen: (listener: Listener) => {stopListen: () => boolean}; - listenOnce: (listener: Listener) => {stopListenOnce: () => boolean}; - stopListen: (listener: Listener) => boolean; - stopListenOnce: (listener: Listener) => boolean; - } - - export interface Empty { - listen: (listener: ListenerEmpty) => {stopListen: () => boolean}; - listenOnce: (listener: ListenerEmpty) => {stopListenOnce: () => boolean}; - stopListen: (listener: ListenerEmpty) => boolean; - stopListenOnce: (listener: ListenerEmpty) => boolean; - } - - type Return = {signal: WithData; send: (data: T) => void; stopListenAll: () => void}; - type ReturnEmpty = {signal: Empty; send: () => void; stopListenAll: () => void}; - - class Create { - withData(): Return { - const signal = new WithDataClass(); - - return { - signal, - send: signal.send, - stopListenAll: signal.stopListenAll, - }; - } - - empty(): ReturnEmpty { - const signal = new EmptyClass(); - - return { - signal, - send: signal.send, - stopListenAll: signal.stopListenAll, - }; - } - } - - export const create = new Create(); -} diff --git a/packages/@webex/plugin-cc/src/services/core/Utils.ts b/packages/@webex/plugin-cc/src/services/core/Utils.ts index 8f5a05b722e..3228b88df98 100644 --- a/packages/@webex/plugin-cc/src/services/core/Utils.ts +++ b/packages/@webex/plugin-cc/src/services/core/Utils.ts @@ -1,6 +1,7 @@ import * as Err from './Err'; import {WebexRequestPayload} from '../../types'; -import {WCC_API_GATEWAY} from '../constants'; +import {Failure} from './GlobalTypes'; +import LoggerProxy from '../../logger-proxy'; const getCommonErrorDetails = (errObj: WebexRequestPayload) => { return { @@ -9,12 +10,15 @@ const getCommonErrorDetails = (errObj: WebexRequestPayload) => { }; }; +export const getErrorDetails = (error: any, methodName: string) => { + const failure = error.details as Failure; + LoggerProxy.logger.error(`${methodName} failed with trackingId: ${failure?.trackingId}`); + + return new Error(failure?.data?.reason ?? `Error while performing ${methodName}`); +}; + export const createErrDetailsObject = (errObj: WebexRequestPayload) => { const details = getCommonErrorDetails(errObj); return new Err.Details('Service.reqs.generic.failure', details); }; - -export const getRoutingHost = () => { - return `${WCC_API_GATEWAY}`; -}; 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 6daf1572e1f..057f041ac80 100644 --- a/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts +++ b/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts @@ -1,22 +1,13 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import {Signal} from './Signal'; 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'; -export const TIMEOUT_REQ = 20000; -const TIMEOUT_EVT = TIMEOUT_REQ; -const FC_DESKTOP_VIEW = 'FC-DESKTOP-VIEW'; -const fcDesktopView = 'fcDesktopView'; - -export class AqmReqs { +export default class AqmReqs { private pendingRequests: Record = {}; - private pendingEvents: Record = {}; - // pendingNotifCancelrequest is used for handling request cancel events based on another action. When a promise can be resolved using multiple events. - // example consult and cancelCtq(end in new API) when we cancel consult to ques we need to resolve both consul and cancel ctq promises - // #CX-14258 : Consult to DN failing if Consult to Queue is cancelled in the first try #2344 private pendingNotifCancelrequest: Record = {}; private httpRequest: HttpRequest; constructor() { @@ -35,47 +26,6 @@ export class AqmReqs { return (cbRes?: CbRes) => this.makeAPIRequest(c(), cbRes); } - evt(p: EvtConf): EvtRes { - const {send, signal} = Signal.create.withData(); - - const k = this.bindPrint(p.bind); - if (this.pendingEvents[k]) { - throw new Err.Details('Service.aqm.reqs.PendingEvent', {key: k}); - } - this.pendingEvents[k] = { - check: (msg: Msg) => this.bindCheck(p.bind, msg), - handle: (msg: any) => send(msg), - }; - - // add listenOnceAsync - const evt: EvtRes = signal as any; - evt.listenOnceAsync = (promise?: {resolveIf?: (msg: T) => boolean; timeout?: Timeout}) => { - return new Promise((resolve, reject) => { - const {stopListen} = signal.listen((msg) => { - if (promise?.resolveIf ? promise.resolveIf(msg) : true) { - stopListen(); - resolve(msg); - } - }); - - if (promise?.timeout === 'disabled') { - return; - } - - const ms = - promise && promise.timeout && promise.timeout > 0 ? promise.timeout : TIMEOUT_EVT; - setTimeout(() => { - const isStopped = stopListen(); - if (isStopped) { - reject(new Err.Details('Service.aqm.reqs.TimeoutEvent', {key: k})); - } - }, ms); - }); - }; - - return evt; - } - private async makeAPIRequest(c: Req, cbRes?: CbRes): Promise { return this.createPromise(c, cbRes); } @@ -153,7 +103,7 @@ export class AqmReqs { }, }; } - let resAxios: WebexRequestPayload | null = null; + let response: WebexRequestPayload | null = null; this.httpRequest .request({ service: c.host ?? '', @@ -164,21 +114,21 @@ export class AqmReqs { body: c.data, }) .then((res: any) => { - resAxios = res; + response = res; if (cbRes) { cbRes(res); } }) - .catch((axiosErr: WebexRequestPayload) => { + .catch((error: WebexRequestPayload) => { clear(); - if (axiosErr?.headers) { - axiosErr.headers.Authorization = '*'; + if (error?.headers) { + error.headers.Authorization = '*'; } - if (axiosErr?.headers) { - axiosErr.headers.Authorization = '*'; + if (error?.headers) { + error.headers.Authorization = '*'; } if (typeof c.err === 'function') { - reject(c.err(axiosErr)); + reject(c.err(error)); } else if (typeof c.err === 'string') { reject(new Err.Message(c.err)); } else { @@ -193,14 +143,14 @@ export class AqmReqs { return; } clear(); - if (resAxios?.headers) { - resAxios.headers.Authorization = '*'; + if (response?.headers) { + response.headers.Authorization = '*'; } - LoggerProxy.logger.error(`Routing request timeout${keySuccess}${resAxios!}${c.url}`); + LoggerProxy.logger.error(`Routing request timeout${keySuccess}${response!}${c.url}`); reject( new Err.Details('Service.aqm.reqs.Timeout', { key: keySuccess, - resAxios: resAxios!, + response: response!, }) ); }, @@ -250,70 +200,6 @@ export class AqmReqs { return true; } - private readonly identifyInteractionIsTaskObject = (event: any) => { - // This method will return the callProcessingDetails are present inside the interaction object or task object - return event?.data?.task?.callProcessingDetails ?? false; - }; - - private isFlowValuesEncrypted(event: any) { - let fcDesktopView1: any; - if (this.identifyInteractionIsTaskObject(event)) { - fcDesktopView1 = event?.data?.task?.callAssociatedData[FC_DESKTOP_VIEW]?.value; - } else { - fcDesktopView1 = event?.data?.interaction?.callAssociatedData[FC_DESKTOP_VIEW]?.value; - } - - return fcDesktopView1?.includes('pop-over') || fcDesktopView1?.includes('interaction-panel'); - } - - private isValidCADFlowValue(event: any) { - // event?.data?.interaction: CAD, CPD values are under the event?.data?.interaction for call events - // event?.data?.task :CAD, CPD values are under the event?.data?.task for monitoring call events - if (this.identifyInteractionIsTaskObject(event)) { - return ( - (event?.data?.task?.callAssociatedData[FC_DESKTOP_VIEW]?.value && - event?.data?.task?.callAssociatedData[FC_DESKTOP_VIEW]?.value !== '') ?? - false - ); - } - // Interaction details are present like event?.data?.interaction - - return ( - (event?.data?.interaction?.callAssociatedData[FC_DESKTOP_VIEW]?.value && - event?.data?.interaction?.callAssociatedData[FC_DESKTOP_VIEW]?.value !== '') ?? - false - ); - } - - private isValidCPDFlowValue(event: any) { - const isDesktopCpdViewEnabled = false; // TODO: revisit this to get the feature flag value if needed by desktop client - // event?.data?.interaction: CAD, CPD values are under the event?.data?.interaction for call events - // event?.data?.task :CAD, CPD values are under the event?.data?.task for monitoring call events - // SERVICE.featureflag.isDesktopCpdViewEnabled() - if (isDesktopCpdViewEnabled) { - if (this.identifyInteractionIsTaskObject(event)) { - return ( - event?.data?.task?.callProcessingDetails[fcDesktopView] && - event?.data?.task?.callProcessingDetails[fcDesktopView] !== '' - ); - } - - return ( - event?.data?.interaction?.callProcessingDetails[fcDesktopView] && - event?.data?.interaction?.callProcessingDetails[fcDesktopView] !== '' - ); - } - - return false; - } - - private getDecompressedValue(encryptedValue: Buffer) { - return encryptedValue; - // TODO: Revisit this to get the decompressSync method from the compression library if needed by desktop client - // const decryptedValue: Uint8Array = decompressSync(encryptedValue); - // return strFromU8(decryptedValue); - } - // must be lambda private readonly onMessage = (event: any) => { // const event = JSON.parse(msg); @@ -329,63 +215,6 @@ export class AqmReqs { return; } - if (this.isValidCADFlowValue(event) && !this.isFlowValuesEncrypted(event)) { - const isTaskObject = this.identifyInteractionIsTaskObject(event); - try { - const targetObject = isTaskObject ? event?.data?.task : event?.data?.interaction; - const encryptedFcValue = targetObject?.callAssociatedData[FC_DESKTOP_VIEW]?.value; - const encryptedValue = Buffer.from(encryptedFcValue, 'base64'); - // Update the decrypted value to same object - targetObject.callAssociatedData[FC_DESKTOP_VIEW].value = - this.getDecompressedValue(encryptedValue); - - const interactionId = targetObject?.interactionId; - LoggerProxy.logger.info( - `${FC_DESKTOP_VIEW} values decrypted successfully for Interaction ID: ${ - interactionId || '' - }` - ); - } catch { - const targetObject = isTaskObject ? event?.data?.task : event?.data?.interaction; - const interactionId = targetObject?.interactionId; - const fcValue = targetObject?.callAssociatedData[FC_DESKTOP_VIEW]?.value || ''; - - LoggerProxy.logger.error( - `Error on decrypting ${FC_DESKTOP_VIEW} value for Interaction Id: ${interactionId}${fcValue}` || - '' - ); - } - } else if (this.isValidCPDFlowValue(event)) { - const isTaskObject = this.identifyInteractionIsTaskObject(event); - try { - // When WXCC_DESKTOP_VIEW_IN_CPD FF is enabled, then values will be fcDesktopView values are always compressed. - const targetObject = isTaskObject ? event?.data?.task : event?.data?.interaction; - const encryptedFcValue = targetObject?.callProcessingDetails[fcDesktopView]; - const encryptedValue = Buffer.from(encryptedFcValue, 'base64'); - - // Update the decrypted value to same object - targetObject.callProcessingDetails[fcDesktopView] = - this.getDecompressedValue(encryptedValue); - - const interactionId = targetObject?.interactionId; - LoggerProxy.logger.info( - `${fcDesktopView} values decrypted successfully for Interaction ID: ${ - interactionId || '' - }` - ); - } catch { - const targetObject = isTaskObject ? event?.data?.task : event?.data?.interaction; - const interactionId = targetObject?.interactionId; - const fcValue = targetObject?.callProcessingDetails[fcDesktopView] || ''; - - LoggerProxy.logger.error( - `Error on decrypting ${fcDesktopView} value for Interaction Id: ${ - interactionId || '' - }${fcValue}` || '' - ); - } - } - let isHandled = false; const kReq = Object.keys(this.pendingRequests); @@ -407,15 +236,7 @@ export class AqmReqs { } } - const kEvt = Object.keys(this.pendingEvents); - for (const thisEvt of kEvt) { - const evt = this.pendingEvents[thisEvt]; - if (evt.check(event)) { - evt.handle(event); - isHandled = true; - break; - } - } + // TODO: add event emitter for unhandled events to replicate event.listen or .on if (!isHandled) { LoggerProxy.logger.info( @@ -424,54 +245,3 @@ export class AqmReqs { } }; } - -type Pending = { - check: (msg: Msg) => boolean; - handle: (msg: Msg) => void; - alternateBind?: string; -}; - -type BindType = string | string[] | {[key: string]: BindType}; -interface Bind { - type: BindType; - data?: any; -} - -type Req = { - url: string; - host?: string; - method?: HTTP_METHODS; - err?: - | ((errObj: WebexRequestPayload) => Err.Details<'Service.reqs.generic.failure'>) - | Err.IdsMessage - | ((e: WebexRequestPayload) => Err.Message | Err.Details); - notifSuccess: {bind: Bind; msg: TRes}; - notifFail?: - | { - bind: Bind; - errMsg: TErr; - err: (e: TErr) => Err.Details; - } - | { - bind: Bind; - errId: Err.IdsDetails; - }; - data?: any; - headers?: Record; - timeout?: Timeout; - notifCancel?: {bind: Bind; msg: TRes}; -}; - -type Timeout = number | 'disabled'; - -type Conf = (p: TReq) => Req; -type ConfEmpty = () => Req; -export type Res = (p: TReq, cbRes?: CbRes) => Promise; -export type ResEmpty = (cbRes?: CbRes) => Promise; -type CbRes = (res: any) => void | TRes; - -// evt -type EvtConf = {bind: Bind; msg: T}; -type EvtRes = Signal.WithData & { - listenOnceAsync: (p?: {resolveIf?: (msg: T) => boolean; timeout?: Timeout}) => Promise; -}; diff --git a/packages/@webex/plugin-cc/src/services/core/config.ts b/packages/@webex/plugin-cc/src/services/core/constants.ts similarity index 88% rename from packages/@webex/plugin-cc/src/services/core/config.ts rename to packages/@webex/plugin-cc/src/services/core/constants.ts index 28e6bb2ad93..59dd0c0a434 100644 --- a/packages/@webex/plugin-cc/src/services/core/config.ts +++ b/packages/@webex/plugin-cc/src/services/core/constants.ts @@ -4,3 +4,4 @@ export const CLOSE_SOCKET_TIMEOUT_DURATION = 16000; export const PING_API_URL = '/health'; export const WELCOME_TIMEOUT = 30000; export const RTD_PING_EVENT = 'rtd-online-status'; +export const TIMEOUT_REQ = 20000; diff --git a/packages/@webex/plugin-cc/src/services/core/service-utils.ts b/packages/@webex/plugin-cc/src/services/core/service-utils.ts deleted file mode 100644 index fcc3897f638..00000000000 --- a/packages/@webex/plugin-cc/src/services/core/service-utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as Err from './Err'; -import {WebexRequestPayload} from '../../types'; - -const getCommonErrorDetails = (errObj: WebexRequestPayload) => { - return { - trackingId: errObj?.headers?.trackingid || errObj?.headers?.TrackingID, - msg: errObj?.body, - }; -}; - -export const createErrDetailsObject = (errObj: WebexRequestPayload) => { - const details = getCommonErrorDetails(errObj); - - return new Err.Details('Service.reqs.generic.failure', details); -}; - -export const handleExternalServiceErrorDetails = (errObj: WebexRequestPayload) => { - const details: {trackingId: string; status?: number} = getCommonErrorDetails(errObj); - - return new Err.Details('Service.reqs.externalService.generic.failure', { - ...details, - status: errObj?.statusCode ?? '', - }); -}; - -export const getCanaryFlagFromSessionStorage = (): boolean => { - const flag = sessionStorage.getItem('canary'); - - return flag === 'true'; -}; - -export const generateUUID = (): string => { - // let d = DateTime.utc().toMillis(); - let d = Date.now(); - - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (d + Math.random() * 16) % 16 | 0; // eslint-disable-line no-bitwise - d = Math.floor(d / 16); - - return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); // eslint-disable-line no-bitwise - }); -}; - -export const RETRY_INTERVAL = 200; -export const sleep = (interval: number) => - new Promise((resolve) => { - setTimeout(() => resolve(true), interval); - }); diff --git a/packages/@webex/plugin-cc/src/services/core/types.ts b/packages/@webex/plugin-cc/src/services/core/types.ts new file mode 100644 index 00000000000..59b83a91992 --- /dev/null +++ b/packages/@webex/plugin-cc/src/services/core/types.ts @@ -0,0 +1,48 @@ +import {HTTP_METHODS, RequestBody, WebexRequestPayload} from '../../types'; +import * as Err from './Err'; +import {Msg} from './GlobalTypes'; + +export type Pending = { + check: (msg: Msg) => boolean; + handle: (msg: Msg) => void; + alternateBind?: string; +}; + +export type BindType = string | string[] | {[key: string]: BindType}; +interface Bind { + type: BindType; + data?: any; +} + +export type Timeout = number | 'disabled'; + +export type Req = { + url: string; + host?: string; + method?: HTTP_METHODS; + err?: + | ((errObj: WebexRequestPayload) => Err.Details<'Service.reqs.generic.failure'>) + | Err.IdsMessage + | ((e: WebexRequestPayload) => Err.Message | Err.Details); + notifSuccess: {bind: Bind; msg: TRes}; + notifFail?: + | { + bind: Bind; + errMsg: TErr; + err: (e: TErr) => Err.Details; + } + | { + bind: Bind; + errId: Err.IdsDetails; + }; + data?: RequestBody; + headers?: Record; + timeout?: Timeout; + notifCancel?: {bind: Bind; msg: TRes}; +}; + +export type Conf = (p: TReq) => Req; +export type ConfEmpty = () => Req; +export type Res = (p: TReq, cbRes?: CbRes) => Promise; +export type ResEmpty = (cbRes?: CbRes) => Promise; +export type CbRes = (res: any) => void | TRes; diff --git a/packages/@webex/plugin-cc/src/services/index.ts b/packages/@webex/plugin-cc/src/services/index.ts index 2f46f8c2e11..6462c0d221a 100644 --- a/packages/@webex/plugin-cc/src/services/index.ts +++ b/packages/@webex/plugin-cc/src/services/index.ts @@ -1,19 +1,13 @@ import routingAgent from './agent'; -import {AqmReqs} from './core/aqm-reqs'; - -export class Services { - // private readonly notifs: AqmNotifs; +import AqmReqs from './core/aqm-reqs'; +export default class Services { public readonly agent: ReturnType; - // readonly configs: ReturnType; private static instance: Services; constructor() { - // this.notifs = new AqmNotifs(); const aqmReq = new AqmReqs(); this.agent = routingAgent(aqmReq); - - // this.configs = aqmConfigs(httpRequest); } public static getInstance(): Services { @@ -24,4 +18,3 @@ export class Services { return this.instance; } } -export default Services; diff --git a/packages/@webex/plugin-cc/src/types.ts b/packages/@webex/plugin-cc/src/types.ts index b34e94a3dd6..36e2ac9c859 100644 --- a/packages/@webex/plugin-cc/src/types.ts +++ b/packages/@webex/plugin-cc/src/types.ts @@ -1,5 +1,5 @@ import {CallingClientConfig} from '@webex/calling/dist/types/CallingClient/types'; -import {IAgentProfile} from './features/types'; +import * as Agent from './services/agent/types'; type Enum> = T[keyof T]; @@ -153,4 +153,149 @@ export type SubscribeRequest = { allowMultiLogin: boolean; }; +/** + * Represents the response from getListOfTeams method. + * + * @public + */ +export type Team = { + /** + * ID of the team. + */ + id: string; + + /** + * Name of the Team. + */ + name: string; +}; + +/** + * Represents AuxCode. + * @public + */ + +export type AuxCode = { + /** + * ID of the Auxiliary Code. + */ + id: string; + + /** + * Indicates whether the auxiliary code is active or not active. + */ + active: boolean; + + /** + * Indicates whether this is the default code (true) or not (false). + */ + defaultCode: boolean; + + /** + * Indicates whether this is the system default code (true) or not (false). + */ + isSystemCode: boolean; + + /** + * A short description indicating the context of the code. + */ + description: string; + + /** + * Name for the Auxiliary Code. + */ + name: string; + + /** + * Indicates the work type associated with this code.. + */ + + workTypeCode: string; +}; + +/** + * Represents the response from AgentConfig. + * + * @public + */ +export type IAgentProfile = { + /** + * The id of the agent. + */ + + agentId: string; + + /** + * The name of the agent. + */ + agentName: string; + + /** + * Identifier for a Desktop Profile. + */ + agentProfileId: string; + + /** + * The email address of the agent. + */ + + agentMailId: string; + + /** + * Represents list of teams of an agent. + */ + teams: Team[]; + + /** + * Represents the voice options of an agent. + */ + + loginVoiceOptions: string[]; + + /** + * Represents the Idle codes list that the agents can select in Agent Desktop.t. + */ + + idleCodes: AuxCode[]; + + /** + * Represents the wrap-up codes list that the agents can select when they wrap up a contact. + */ + wrapUpCodes: AuxCode[]; +}; + export type EventResult = IAgentProfile; + +/** + * Represents the request to a AgentLogin + * + * @public + */ +export type AgentLogin = { + /** + * A dialNumber field contains the number to dial such as a route point or extension. + */ + + dialNumber?: string; + + /** + * The unique ID representing a team of users. + */ + + teamId: string; + + /** + * The loginOption field contains the type of login. + */ + + loginOption: LoginOption; +}; +export type RequestBody = + | SubscribeRequest + | Agent.Logout + | Agent.UserStationLogin + | Agent.StateChange; + +export type StationLoginResponse = Agent.StationLoginSuccess | Error; +export type StationLogoutResponse = Agent.LogoutSuccess | Error; +export type StationReLoginResponse = Agent.ReloginSuccess | Error; diff --git a/packages/@webex/plugin-cc/test/unit/spec/cc.ts b/packages/@webex/plugin-cc/test/unit/spec/cc.ts index f645821dfd4..8f08ef1b3d5 100644 --- a/packages/@webex/plugin-cc/test/unit/spec/cc.ts +++ b/packages/@webex/plugin-cc/test/unit/spec/cc.ts @@ -1,15 +1,13 @@ import 'jsdom-global/register'; -import {LoginOption, WebexSDK} from '../../../src/types'; -import HttpRequest from '../../../src/services/core/HttpRequest'; -import WebCallingService from '../../../src/WebCallingService'; +import {LoginOption, StationLogoutResponse, WebexSDK} from '../../../src/types'; import ContactCenter from '../../../src/cc'; import MockWebex from '@webex/test-helper-mock-webex'; import {StationLoginSuccess} from '../../../src/services/agent/types'; -import {IAgentProfile} from '../../../src/features/types'; +import {IAgentProfile} from '../../../src/types'; import {AGENT, WEB_RTC_PREFIX} from '../../../src/services/constants'; import Services from '../../../src/services'; import config from '../../../src/config'; -import {web} from 'webpack'; +import LoggerProxy from '../../../src/logger-proxy'; jest.mock('../../../src/logger-proxy', () => ({ __esModule: true, @@ -24,7 +22,7 @@ jest.mock('../../../src/logger-proxy', () => ({ jest.mock('../../../src/services/config'); jest.mock('../../../src/services/core/HttpRequest'); -jest.mock('../../../src/WebCallingService'); +jest.mock('../../../src/services/WebCallingService'); jest.mock('../../../src/services'); // Mock AgentConfig @@ -61,6 +59,8 @@ describe('webex.cc', () => { const mockServicesInstance = { agent: { stationLogin: jest.fn(), + logout: jest.fn(), + reload: jest.fn(), }, }; (Services.getInstance as jest.Mock).mockReturnValue(mockServicesInstance); @@ -134,7 +134,7 @@ describe('webex.cc', () => { }); it('should log error and reject if registration fails', async () => { - const mockError = new Error('Registration failed'); + const mockError = new Error('Error while performing register'); mockHttpRequest.subscribeNotifications.mockRejectedValue(mockError); await expect(webex.cc.register()).rejects.toThrow('Error while performing register'); @@ -222,11 +222,90 @@ describe('webex.cc', () => { dialNumber: '1234567890', }; - const error = new Error('Error while performing station login'); + const error = { + details: { + trackingId: '1234', + data: { + reason: 'Error while performing station login', + }, + }, + }; jest.spyOn(webex.cc.services.agent, 'stationLogin').mockRejectedValue(error); - await expect(webex.cc.stationLogin(options)).rejects.toThrow( - 'Error while performing station login' + await expect(webex.cc.stationLogin(options)).rejects.toThrow(error.details.data.reason); + + expect(LoggerProxy.logger.error).toHaveBeenCalledWith( + `stationLogin failed with trackingId: ${error.details.trackingId}` + ); + }); + }); + + describe('stationLogout', () => { + it('should logout successfully', async () => { + const data = {logoutReason: 'Logout reason'}; + const response = {}; + + const stationLogoutMock = jest + .spyOn(webex.cc.services.agent, 'logout') + .mockResolvedValue({} as StationLogoutResponse); + + const result = await webex.cc.stationLogout(data); + + expect(stationLogoutMock).toHaveBeenCalledWith({data: data}); + expect(result).toEqual(response); + }); + + it('should handle error during stationLogout', async () => { + const data = {logoutReason: 'Logout reason'}; + const error = { + details: { + trackingId: '1234', + data: { + reason: 'Error while performing station logout', + }, + }, + }; + + jest.spyOn(webex.cc.services.agent, 'logout').mockRejectedValue(error); + + await expect(webex.cc.stationLogout(data)).rejects.toThrow(error.details.data.reason); + + expect(LoggerProxy.logger.error).toHaveBeenCalledWith( + `stationLogout failed with trackingId: ${error.details.trackingId}` + ); + }); + }); + + describe('stationRelogin', () => { + it('should relogin successfully', async () => { + const response = {}; + + const stationLoginMock = jest + .spyOn(webex.cc.services.agent, 'reload') + .mockResolvedValue({} as StationLoginSuccess); + + const result = await webex.cc.stationReLogin(); + + expect(stationLoginMock).toHaveBeenCalled(); + expect(result).toEqual(response); + }); + + it('should handle error during relogin', async () => { + const error = { + details: { + trackingId: '1234', + data: { + reason: 'Error while performing station relogin', + }, + }, + }; + + jest.spyOn(webex.cc.services.agent, 'reload').mockRejectedValue(error); + + await expect(webex.cc.stationReLogin()).rejects.toThrow(error.details.data.reason); + + expect(LoggerProxy.logger.error).toHaveBeenCalledWith( + `stationReLogin failed with trackingId: ${error.details.trackingId}` ); }); }); diff --git a/packages/@webex/plugin-cc/test/unit/spec/WebCallingService.ts b/packages/@webex/plugin-cc/test/unit/spec/services/WebCallingService.ts similarity index 97% rename from packages/@webex/plugin-cc/test/unit/spec/WebCallingService.ts rename to packages/@webex/plugin-cc/test/unit/spec/services/WebCallingService.ts index cf47ada2f52..0fe0bc476ed 100644 --- a/packages/@webex/plugin-cc/test/unit/spec/WebCallingService.ts +++ b/packages/@webex/plugin-cc/test/unit/spec/services/WebCallingService.ts @@ -1,7 +1,7 @@ import 'jsdom-global/register'; -import WebCallingService from '../../../src/WebCallingService'; +import WebCallingService from '../../../../src/services/WebCallingService'; import {createClient, ICallingClient, ILine, LINE_EVENTS, ICall} from '@webex/calling'; -import {WebexSDK} from '../../../src/types'; +import {WebexSDK} from '../../../../src/types'; jest.mock('@webex/calling'); diff --git a/packages/@webex/plugin-cc/test/unit/spec/services/agent/index.ts b/packages/@webex/plugin-cc/test/unit/spec/services/agent/index.ts index 5e44ec0e4bc..522f0c04276 100644 --- a/packages/@webex/plugin-cc/test/unit/spec/services/agent/index.ts +++ b/packages/@webex/plugin-cc/test/unit/spec/services/agent/index.ts @@ -1,31 +1,58 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import {AqmReqs} from '../../../../../src/services/core/aqm-reqs'; import routingAgent from '../../../../../src/services/agent'; +import AqmReqs from '../../../../../src/services/core/aqm-reqs'; +import * as Utils from '../../../../../src/services/core/Utils'; -jest.mock('../../../../../src/services/core/HttpRequest'); -jest.mock('../../../../../src/services/core/aqm-reqs'); +jest.mock('../../../../../src/services/core/Utils', () => ({ + createErrDetailsObject: jest.fn(), + getRoutingHost: jest.fn(), +})); -const fakeAqm = new AqmReqs(); -const agent = routingAgent(fakeAqm) as any; +jest.mock('../../../../../src/services/core/aqm-reqs'); describe('AQM routing agent', () => { + let fakeAqm: jest.Mocked; + let agent: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + + const getroutingSpy = jest + .spyOn(Utils, 'getRoutingHost') + .mockReturnValue('https://mock-routing-host.com'); + + fakeAqm = new AqmReqs() as jest.Mocked; + fakeAqm.reqEmpty = jest.fn().mockImplementation((fn) => fn); + fakeAqm.req = jest.fn().mockImplementation((fn) => fn); + + agent = routingAgent(fakeAqm); + }); + it('logout', async () => { - const req = agent.logout({data: {logoutReason: 'User requested logout'}}).catch((e: any) => e); + const reqSpy = jest.spyOn(fakeAqm, 'reqEmpty'); + reqSpy.mockRejectedValue(new Error('dasd')); + const req = await agent.logout({data: {logoutReason: 'User requested logout'}}); expect(req).toBeDefined(); + expect(reqSpy).toHaveBeenCalled(); }); it('reload', async () => { - const req = agent.reload().catch((e: any) => e); + const reqSpy = jest.spyOn(fakeAqm, 'reqEmpty'); + const req = await agent.reload(); expect(req).toBeDefined(); + expect(reqSpy).toHaveBeenCalled(); }); it('stationLogin', async () => { - const req = agent.stationLogin({data: {} as any}).catch((e: any) => e); + const reqSpy = jest.spyOn(fakeAqm, 'req'); + const req = await agent.stationLogin({data: {} as any}); expect(req).toBeDefined(); + expect(reqSpy).toHaveBeenCalled(); }); it('stateChange', async () => { - const req = agent.stateChange({data: {} as any}).catch((e: any) => e); + const reqSpy = jest.spyOn(fakeAqm, 'req'); + const req = await agent.stateChange({data: {} as any}); expect(req).toBeDefined(); + expect(reqSpy).toHaveBeenCalled(); }); }); diff --git a/packages/@webex/plugin-cc/test/unit/spec/services/core/HttpRequest.ts b/packages/@webex/plugin-cc/test/unit/spec/services/core/HttpRequest.ts index 8e65060f271..48b2cb72ab2 100644 --- a/packages/@webex/plugin-cc/test/unit/spec/services/core/HttpRequest.ts +++ b/packages/@webex/plugin-cc/test/unit/spec/services/core/HttpRequest.ts @@ -1,10 +1,7 @@ import HttpRequest from '../../../../../src/services/core/HttpRequest'; import {CC_EVENTS, SubscribeResponse} from '../../../../../src/services/config/types'; import {WEBSOCKET_EVENT_TIMEOUT} from '../../../../../src/services/constants'; -import MockWebex from '@webex/test-helper-mock-webex'; -import WebSocket from '../../../../../src/services/core/WebSocket'; -import {EventEmitter} from 'events'; -import {HTTP_METHODS, WebexSDK} from '../../../../../src/types'; +import {WebexSDK} from '../../../../../src/types'; jest.mock('../../../../../src/services/core/WebSocket'); @@ -75,113 +72,4 @@ describe('HttpRequest', () => { WEBSOCKET_EVENT_TIMEOUT + 1000 ); // Increase timeout for this test }); - - describe('sendRequestWithEvent', () => { - it('should resolve the promise when the specified event type is received', async () => { - const mockResponse = { - status: 200, - body: {}, - }; - const mockEvent = { - type: 'SUCCESS_EVENT', - data: {message: 'Success'}, - }; - - mockRequest.mockResolvedValueOnce(mockResponse); - - setTimeout(() => { - httpRequest.eventHandlers.get('EVENT_TYPE')(mockEvent); - }, 100); - - const result = await httpRequest.sendRequestWithEvent({ - service: 'service', - resource: 'resource', - method: HTTP_METHODS.POST, - payload: {}, - eventType: 'EVENT_TYPE', - success: ['SUCCESS_EVENT'], - failure: ['FAILURE_EVENT'], - }); - expect(result).toEqual(mockEvent); - }); - - it('should reject the promise when a failure event type is received', async () => { - const mockResponse = { - status: 200, - body: {}, - }; - const mockEvent = { - type: 'FAILURE_EVENT', - data: {reason: 'Failed'}, - }; - - mockRequest.mockResolvedValueOnce(mockResponse); - - setTimeout(() => { - httpRequest.eventHandlers.get('EVENT_TYPE')(mockEvent); - }, 100); - - await expect( - httpRequest.sendRequestWithEvent({ - service: 'service', - resource: 'resource', - method: HTTP_METHODS.POST, - payload: {}, - eventType: 'EVENT_TYPE', - success: ['SUCCESS_EVENT'], - failure: ['FAILURE_EVENT'], - }) - ).rejects.toThrow('FAILURE_EVENT'); - }); - - it('should reject the promise when an unexpected event type is received', async () => { - const mockResponse = { - status: 200, - body: {}, - }; - const mockEvent = { - type: 'UNEXPECTED_EVENT', - data: {message: 'Unexpected'}, - }; - - mockRequest.mockResolvedValueOnce(mockResponse); - - setTimeout(() => { - httpRequest.eventHandlers.get('EVENT_TYPE')(mockEvent); - }, 100); - - await expect( - httpRequest.sendRequestWithEvent({ - service: 'service', - resource: 'resource', - method: HTTP_METHODS.POST, - payload: {}, - eventType: 'EVENT_TYPE', - success: ['SUCCESS_EVENT'], - failure: ['FAILURE_EVENT'], - }) - ).rejects.toThrow('Unexpected event type received: UNEXPECTED_EVENT'); - }); - - it('should log and throw an error if the service request fails', async () => { - const mockError = new Error('Request failed'); - mockRequest.mockRejectedValueOnce(mockError); - - await expect( - httpRequest.sendRequestWithEvent({ - service: 'service', - resource: 'resource', - method: HTTP_METHODS.POST, - payload: {}, - eventType: 'EVENT_TYPE', - success: ['SUCCESS_EVENT'], - failure: ['FAILURE_EVENT'], - }) - ).rejects.toThrow('Request failed'); - - expect(mockWebex.logger.error).toHaveBeenCalledWith( - 'Error sending service request: Error: Request failed' - ); - }); - }); }); diff --git a/packages/@webex/plugin-cc/test/unit/spec/services/core/aqm-reqs.ts b/packages/@webex/plugin-cc/test/unit/spec/services/core/aqm-reqs.ts index 593f9cd1159..321b46cc09c 100644 --- a/packages/@webex/plugin-cc/test/unit/spec/services/core/aqm-reqs.ts +++ b/packages/@webex/plugin-cc/test/unit/spec/services/core/aqm-reqs.ts @@ -1,11 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as config from '../../../../../src/services/core/config'; -import {AqmReqs} from '../../../../../src/services/core/aqm-reqs'; -import {HTTP_METHODS} from '../../../../../src/types'; +import AqmReqs from '../../../../../src/services/core/aqm-reqs'; import HttpRequest from '../../../../../src/services/core/HttpRequest'; import LoggerProxy from '../../../../../src/logger-proxy'; -import * as Err from '../../../../../src/services/core/Err'; -import {Msg} from '../../../../../src/services/core/GlobalTypes'; jest.mock('../../../../../src/services/core/HttpRequest'); jest.mock('../../../../../src/logger-proxy', () => ({ @@ -14,6 +10,7 @@ jest.mock('../../../../../src/logger-proxy', () => ({ logger: { log: jest.fn(), error: jest.fn(), + info: jest.fn(), }, initialize: jest.fn(), }, @@ -323,4 +320,62 @@ describe('AqmReqs', () => { expect(p).toBeDefined(); } catch (e) {} }); + + it('should handle onMessage with Welcome event', () => { + const mockWebSocket = { + on: jest.fn(), + }; + + httpRequestInstance.getWebSocket = jest.fn().mockReturnValue(mockWebSocket); + + const aqm = new AqmReqs(); + + const event = { + type: 'Welcome', + }; + + aqm['onMessage'](event); + + expect(LoggerProxy.logger.info).toHaveBeenCalledWith( + 'Welcome message from Notifs Websocket[object Object]' + ); + }); + + it('should handle onMessage with Keepalive event', () => { + const mockWebSocket = { + on: jest.fn(), + }; + + httpRequestInstance.getWebSocket = jest.fn().mockReturnValue(mockWebSocket); + + const aqm = new AqmReqs(); + + const event = { + keepalive: true, + }; + + aqm['onMessage'](event); + + expect(LoggerProxy.logger.info).toHaveBeenCalledWith('Keepalive from notifs[object Object]'); + }); + + it('should handle onMessage with missing event handler', () => { + const mockWebSocket = { + on: jest.fn(), + }; + + httpRequestInstance.getWebSocket = jest.fn().mockReturnValue(mockWebSocket); + + const aqm = new AqmReqs(); + + const event = { + type: 'UnknownEvent', + }; + + aqm['onMessage'](event); + + expect(LoggerProxy.logger.info).toHaveBeenCalledWith( + 'event=missingEventHandler | [AqmReqs] missing routing message handler[object Object]' + ); + }); }); diff --git a/packages/calling/src/index.ts b/packages/calling/src/index.ts index cdb92097f7e..1673dfccb6f 100644 --- a/packages/calling/src/index.ts +++ b/packages/calling/src/index.ts @@ -52,3 +52,4 @@ export {CallError, LineError} from './Errors'; export {ICall, TransferType} from './CallingClient/calling/types'; export {LOGGER} from './Logger/types'; export {LocalMicrophoneStream} from '@webex/media-helpers'; +export {CallingClientConfig} from './CallingClient/types';