diff --git a/docs/samples/calling/app.js b/docs/samples/calling/app.js index ec8a6c71c97..cad89050736 100644 --- a/docs/samples/calling/app.js +++ b/docs/samples/calling/app.js @@ -308,13 +308,13 @@ function toggleDisplay(elementId, status) { } } -const callNotifyEvent = new CustomEvent('callingClient:incoming_call', { +const callNotifyEvent = new CustomEvent('line:incoming_call', { detail: { callObject: call, }, }); -callListener.addEventListener('callingClient:incoming_call', (myEvent) => { +callListener.addEventListener('line:incoming_call', (myEvent) => { console.log('Received incoming call'); answerElm.disabled = false; const callerDisplay = myEvent.detail.callObject.getCallerInfo(); @@ -345,9 +345,9 @@ function createDevice() { }); // Start listening for incoming calls - callingClient.on('callingClient:incoming_call', (callObj) => { + line.on('line:incoming_call', (callObj) => { call = callObj; - call.on('call:caller_id', (CallerIdEmitter) => { + call.on('caller_id', (CallerIdEmitter) => { callDetailsElm.innerText = `Name: ${CallerIdEmitter.callerId.name}, Number: ${CallerIdEmitter.callerId.num}, Avatar: ${CallerIdEmitter.callerId.avatarSrc}, UserId: ${CallerIdEmitter.callerId.id}`; console.log( `callerId : Name: ${CallerIdEmitter.callerId.name}, Number: ${CallerIdEmitter.callerId.name}, Avatar: ${CallerIdEmitter.callerId.avatarSrc}, UserId: ${CallerIdEmitter.callerId.id}` @@ -407,14 +407,14 @@ function muteUnmute() { function holdResume() { const elem = document.getElementById('hold_button'); - call.on('call:held', (correlationId) => { + call.on('held', (correlationId) => { if (elem.value === 'Hold') { callDetailsElm.innerText = 'Call is held'; elem.value = 'Resume'; } }); - call.on('call:resumed', (correlationId) => { + call.on('resumed', (correlationId) => { if (elem.value === 'Resume') { callDetailsElm.innerText = 'Call is Resumed'; elem.value = 'Hold'; @@ -477,12 +477,13 @@ function createCall(e) { console.log(destination.value); - call = callingClient.makeCall({ + + call = line.makeCall({ type: 'uri', address: destination.value, }); - call.on('call:caller_id', (CallerIdEmitter) => { + call.on('caller_id', (CallerIdEmitter) => { callDetailsElm.innerText = `Name: ${CallerIdEmitter.callerId.name}, Number: ${CallerIdEmitter.callerId.num}, Avatar: ${CallerIdEmitter.callerId.avatarSrc} , UserId: ${CallerIdEmitter.callerId.id}`; console.log( `callerId : Name: ${CallerIdEmitter.callerId.name}, Number: ${CallerIdEmitter.callerId.num}, Avatar: ${CallerIdEmitter.callerId.avatarSrc}, UserId: ${CallerIdEmitter.callerId.id}` @@ -493,19 +494,19 @@ function createCall(e) { } }); - call.on('call:progress', (correlationId) => { + call.on('progress', (correlationId) => { callDetailsElm.innerText = `${correlationId}: Call Progress`; }); - call.on('call:connect', (correlationId) => { + call.on('connect', (correlationId) => { callDetailsElm.innerText = `${correlationId}: Call Connect`; }); - call.on('call:established', (correlationId) => { + call.on('established', (correlationId) => { callDetailsElm.innerText = `${correlationId}: Call Established`; transferElm.disabled = false; outboundEndElm.disabled = false; makeCallBtn.disabled = true; }); - call.on('call:disconnect', (correlationId) => { + call.on('disconnect', (correlationId) => { callDetailsElm.innerText = `${correlationId}: Call Disconnected`; makeCallBtn.disabled = false; endElm.disabled = true; @@ -519,7 +520,7 @@ function createCall(e) { } }); - call.on('call:remote_media', (track) => { + call.on('remote_media', (track) => { document.getElementById('remote-audio').srcObject = new MediaStream([track]); }); @@ -561,20 +562,20 @@ function commitTransfer() { address: digit, }); - callTranferObj.on('call:remote_media', (track) => { + callTranferObj.on('remote_media', (track) => { document.getElementById('remote-audio').srcObject = new MediaStream([track]); transferDetailsElm.innerText = `Got remote audio`; }); - callTranferObj.on('call:established', (correlationId) => { + callTranferObj.on('established', (correlationId) => { transferDetailsElm.innerText = `${correlationId}: Transfer target connected`; endSecondElm.disabled = false; transferElm.innerHTML = 'Commit'; transferElm.disabled = false; }); - callTranferObj.on('call:disconnect', (correlationId) => { + callTranferObj.on('disconnect', (correlationId) => { endSecondElm.disabled = true; callTranferObj = null; }); @@ -595,7 +596,7 @@ function initiateTransfer() { if (!call.isHeld()) { call.doHoldResume(); - call.on('call:held', (correlationId) => { + call.on('held', (correlationId) => { transferDetailsElm.innerText = `Placed call: ${call.getCorrelationId()} on hold`; commitTransfer(); }); @@ -677,16 +678,16 @@ function answer() { answerElm.disabled = true; if (call) { - call.on('call:established', (correlationId) => { + call.on('established', (correlationId) => { callDetailsElm.innerText = `${correlationId}: Call Established`; console.log(` Call is Established: ${correlationId}`); endElm.disabled = false; }); - call.on('call:disconnect', () => { + call.on('disconnect', () => { console.log(` Call is Disconnected: ${correlationId}`); }); - call.on('call:remote_media', (track) => { + call.on('remote_media', (track) => { document.getElementById('remote-audio').srcObject = new MediaStream([track]); }); diff --git a/packages/calling/src/CallingClient/CallingClient.test.ts b/packages/calling/src/CallingClient/CallingClient.test.ts index d004e551d65..d88f673a96a 100644 --- a/packages/calling/src/CallingClient/CallingClient.test.ts +++ b/packages/calling/src/CallingClient/CallingClient.test.ts @@ -1,12 +1,12 @@ +import {Mutex} from 'async-mutex'; import {LOGGER} from '../Logger/types'; -import {getTestUtilsWebex, getMockRequestTemplate, getMockDeviceInfo} from '../common/testUtil'; import { - CallDirection, - CallType, - MobiusStatus, - ServiceIndicator, - WebexRequestPayload, -} from '../common/types'; + getTestUtilsWebex, + getMockRequestTemplate, + getMockDeviceInfo, + getMobiusDiscoveryResponse, +} from '../common/testUtil'; +import {CallType, MobiusStatus, ServiceIndicator, WebexRequestPayload} from '../common/types'; /* eslint-disable dot-notation */ import {CallSessionEvent, EVENT_KEYS, MOBIUS_EVENT_KEYS} from '../Events/types'; import log from '../Logger'; @@ -23,7 +23,7 @@ import { SPARK_USER_AGENT, } from './constants'; import {MOCK_MULTIPLE_SESSIONS_EVENT, MOCK_SESSION_EVENT} from './callRecordFixtures'; -import {ILine} from './line/types'; +import {ILine, LineStatus} from './line/types'; import { ipPayload, regionBody, @@ -34,6 +34,10 @@ import { uri, myIP, } from './callingClientFixtures'; +import Line from './line'; +import {filterMobiusUris} from '../common/Utils'; +import {URL} from './registration/registerFixtures'; +import {ICall} from './calling/types'; describe('CallingClient Tests', () => { // Common initializers @@ -571,10 +575,31 @@ describe('CallingClient Tests', () => { // Calling related test cases describe('Calling tests', () => { - let callingClient: ICallingClient; + const mutex = new Mutex(); + const userId = webex.internal.device.userId; + const clientDeviceUri = webex.internal.device.url; + const mobiusUris = filterMobiusUris(getMobiusDiscoveryResponse(), URL); + const primaryMobiusUris = jest.fn(() => mobiusUris.primary); + const backupMobiusUris = jest.fn(() => mobiusUris.backup); + + let callingClient; + let line: Line; beforeAll(async () => { - callingClient = await createClient(webex, {logger: {level: LOGGER.INFO}}); + callingClient = await createClient(webex); + line = new Line( + userId, + clientDeviceUri, + LineStatus.ACTIVE, + mutex, + primaryMobiusUris(), + backupMobiusUris(), + LOGGER.INFO + ); + const calls = Object.values(callManager.getActiveCalls()); + calls.forEach((call) => { + call.end(); + }); }); afterAll(() => { @@ -588,85 +613,60 @@ describe('CallingClient Tests', () => { ); }); - it('Return a successful call object while making call', () => { - const call = callingClient.makeCall({address: '5003', type: CallType.URI}); - - expect(call).toBeTruthy(); - expect(callingClient.getCall(call ? call.getCorrelationId() : '')).toBe(call); - expect(call ? call['direction'] : undefined).toStrictEqual(CallDirection.OUTBOUND); - call?.end(); + it('returns undefined when there is no connected call', () => { + line.register(); + line.makeCall({address: '123456', type: CallType.URI}); + expect(callingClient.getConnectedCall()).toEqual(undefined); }); - it('Return a successful call object while making call to FAC codes', () => { - const call = callingClient.makeCall({address: '*25', type: CallType.URI}); - - expect(call).toBeTruthy(); - expect(call ? call['direction'] : undefined).toStrictEqual(CallDirection.OUTBOUND); - call?.end(); - }); + it('returns the connected call', () => { + line.register(); + const mockCall = line.makeCall({address: '1234', type: CallType.URI}); + const mockCall2 = line.makeCall({address: '5678', type: CallType.URI}); + // Connected call + mockCall['connected'] = true; + mockCall['earlyMedia'] = false; + mockCall['callStateMachine'].state.value = 'S_CALL_ESTABLISHED'; - it('Remove spaces from dialled number while making call', () => { - const call = callingClient.makeCall({address: '+91 123 456 7890', type: CallType.URI}); + // Held call + mockCall2['connected'] = true; + mockCall2['held'] = true; + mockCall2['earlyMedia'] = false; + mockCall2['callStateMachine'].state.value = 'S_CALL_ESTABLISHED'; - expect(call).toBeTruthy(); - expect(call ? call['direction'] : undefined).toStrictEqual(CallDirection.OUTBOUND); - expect(call ? call['destination']['address'] : undefined).toStrictEqual('tel:+911234567890'); - call?.end(); - }); - - it('Remove hyphen from dialled number while making call', () => { - const call = callingClient.makeCall({address: '123-456-7890', type: CallType.URI}); + const mockActiveCalls: Record = { + mockCorrelationId: mockCall as ICall, + mockCorrelationId2: mockCall2 as ICall, + }; - expect(call).toBeTruthy(); - expect(call ? call['direction'] : undefined).toStrictEqual(CallDirection.OUTBOUND); - expect(call ? call['destination']['address'] : undefined).toStrictEqual('tel:1234567890'); - call?.end(); + jest.spyOn(callManager, 'getActiveCalls').mockReturnValue(mockActiveCalls); + expect(callingClient.getConnectedCall()).toEqual(mockCall); }); + it('returns all active calls', () => { + callingClient.lineDict = { + mockDeviceId: {lineId: 'mockLineId'} as ILine, + mockDeviceId2: {lineId: 'mockLineId2'} as ILine, + }; - it('attempt to create call with incorrect number format 1', (done) => { - // There may be other listeners , which may create race - callingClient.removeAllListeners(EVENT_KEYS.ERROR); - const createCallSpy = jest.spyOn(callManager, 'createCall'); - - callingClient.on(EVENT_KEYS.ERROR, (error) => { - expect(error.message).toBe( - 'An invalid phone number was detected. Check the number and try again.' - ); - done(); - }); - try { - const call = callingClient.makeCall({address: 'select#$@^^', type: CallType.URI}); + const mockCall = line.makeCall({address: '1234', type: CallType.URI}); + const mockCall2 = line.makeCall({address: '5678', type: CallType.URI}); + const mockCall3 = line.makeCall({address: '9101', type: CallType.URI}); - expect(call).toBeUndefined(); - expect(createCallSpy).toBeCalledTimes(0); - } catch (error) { - done(error); - } - expect.assertions(3); - }); + mockCall.lineId = 'mockLineId'; + mockCall2.lineId = 'mockLineId2'; + mockCall3.lineId = 'mockLineId2'; - it('attempt to create call with incorrect number format 2', (done) => { - expect.assertions(3); - // There may be other listeners , which may create race - callingClient.removeAllListeners(EVENT_KEYS.ERROR); - const createCallSpy = jest.spyOn(callManager, 'createCall'); + const mockActiveCalls: Record = { + mockCorrelationId: mockCall as ICall, + mockCorrelationId2: mockCall2 as ICall, + mockCorrelationId3: mockCall3 as ICall, + }; - callingClient.on(EVENT_KEYS.ERROR, (error) => { - expect(error.message).toBe( - 'An invalid phone number was detected. Check the number and try again.' - ); - done(); + jest.spyOn(callManager, 'getActiveCalls').mockReturnValue(mockActiveCalls); + expect(callingClient.getActiveCalls()).toEqual({ + mockLineId: [mockCall], + mockLineId2: [mockCall2, mockCall3], }); - - try { - const call = callingClient.makeCall({address: '+1@8883332505', type: CallType.URI}); - - expect(call).toBeUndefined(); - expect(createCallSpy).toBeCalledTimes(0); - } catch (error) { - done(error); - } - expect.assertions(3); }); }); diff --git a/packages/calling/src/CallingClient/CallingClient.ts b/packages/calling/src/CallingClient/CallingClient.ts index 9aa745c82b8..2f4596ce945 100644 --- a/packages/calling/src/CallingClient/CallingClient.ts +++ b/packages/calling/src/CallingClient/CallingClient.ts @@ -4,7 +4,6 @@ import * as Media from '@webex/internal-media-core'; import {Mutex} from 'async-mutex'; import {filterMobiusUris, handleCallingClientErrors, validateServiceData} from '../common/Utils'; -import {ERROR_TYPE} from '../Errors/types'; import {LOGGER, LogContext} from '../Logger/types'; import SDKConnector from '../SDKConnector'; import {ClientRegionInfo, ISDKConnector, WebexSDK} from '../SDKConnector/types'; @@ -17,10 +16,6 @@ import { SessionType, } from '../Events/types'; import { - MobiusStatus, - CallDirection, - CallDetails, - CorrelationId, ServiceIndicator, RegionInfo, ALLOWED_SERVICES, @@ -35,7 +30,6 @@ import log from '../Logger'; import {getCallManager} from './calling/callManager'; import { CALLING_CLIENT_FILE, - VALID_PHONE, CALLS_CLEARED_HANDLER_UTIL, CALLING_USER_AGENT, CISCO_DEVICE_URL, @@ -46,7 +40,6 @@ import { URL_ENDPOINT, NETWORK_FLAP_TIMEOUT, } from './constants'; -import {CallingClientError} from '../Errors'; import Line from './line'; import {ILine, LINE_EVENTS, LineStatus} from './line/types'; import {METRIC_EVENT, REG_ACTION, METRIC_TYPE, IMetricManager} from '../Metrics/types'; @@ -110,7 +103,6 @@ export class CallingClient extends Eventing implements log.setLogger(logLevel, CALLING_CLIENT_FILE); - this.incomingCallListener(); this.registerCallsClearedListener(); } @@ -123,21 +115,6 @@ export class CallingClient extends Eventing implements this.detectNetworkChange(); } - /** - * An Incoming Call listener. - */ - private incomingCallListener() { - const logContext = { - file: CALLING_CLIENT_FILE, - method: this.incomingCallListener.name, - }; - - log.log('Listening for incoming calls... ', logContext); - this.callManager.on(EVENT_KEYS.INCOMING_CALL, (callObj: ICall) => { - this.emit(EVENT_KEYS.INCOMING_CALL, callObj); - }); - } - /** * Register callbacks for network changes. */ @@ -375,61 +352,6 @@ export class CallingClient extends Eventing implements return log.getLogLevel(); } - /** - * @param callId -. - * @param correlationId -. - */ - public getCall = (correlationId: CorrelationId): ICall => { - return this.callManager.getCall(correlationId); - }; - - /** - * @param dest -. - */ - public makeCall = (dest: CallDetails): ICall | undefined => { - let call; - - // this is a temporary logic to get registration obj - // it will change once we have proper lineId and multiple lines as well - const {registration} = Object.values(this.lineDict)[0]; - - if (dest) { - const match = dest.address.match(VALID_PHONE); - - if (match && match[0].length === dest.address.length) { - const sanitizedNumber = dest.address - .replace(/[^[*+]\d#]/gi, '') - .replace(/\s+/gi, '') - .replace(/-/gi, ''); - const formattedDest = { - type: dest.type, - address: `tel:${sanitizedNumber}`, - }; - - call = this.callManager.createCall( - formattedDest, - CallDirection.OUTBOUND, - registration.getDeviceInfo().device?.deviceId as string - ); - log.log(`New call created, callId: ${call.getCallId()}`, {}); - } else { - log.warn('Invalid phone number detected', {}); - const err = new CallingClientError( - 'An invalid phone number was detected. Check the number and try again.', - {}, - ERROR_TYPE.CALL_ERROR, - MobiusStatus.ACTIVE - ); - - this.emit(EVENT_KEYS.ERROR, err); - } - - return call; - } - - return undefined; - }; - /** * */ @@ -491,6 +413,38 @@ export class CallingClient extends Eventing implements public getLines(): Record { return this.lineDict; } + + /** + * Retrieves call objects for all the active calls present in the client + */ + public getActiveCalls(): Record { + const activeCalls = {}; + const calls = this.callManager.getActiveCalls(); + Object.keys(calls).forEach((correlationId) => { + const call = calls[correlationId]; + if (!activeCalls[call.lineId]) { + activeCalls[call.lineId] = []; + } + activeCalls[call.lineId].push(call); + }); + + return activeCalls; + } + + /** + * Retrieves call object for the connected call in the client + */ + public getConnectedCall(): ICall | undefined { + let connectCall; + const calls = this.callManager.getActiveCalls(); + Object.keys(calls).forEach((correlationId) => { + if (calls[correlationId].isConnected() && !calls[correlationId].isHeld()) { + connectCall = calls[correlationId]; + } + }); + + return connectCall; + } } /** diff --git a/packages/calling/src/CallingClient/calling/call.test.ts b/packages/calling/src/CallingClient/calling/call.test.ts index c3b8f037b8b..d9a88606a37 100644 --- a/packages/calling/src/CallingClient/calling/call.test.ts +++ b/packages/calling/src/CallingClient/calling/call.test.ts @@ -34,6 +34,7 @@ const mockMediaSDK = MediaSDK as jest.Mocked; const defaultServiceIndicator = ServiceIndicator.CALLING; const activeUrl = 'FakeActiveUrl'; +const mockLineId = 'e4e8ee2a-a154-4e52-8f11-ef4cde2dce72'; // class MockMediaStream { // private track; @@ -156,6 +157,7 @@ describe('Call Tests', () => { dest, CallDirection.OUTBOUND, deviceId, + mockLineId, deleteCallFromCollection, defaultServiceIndicator ); @@ -185,7 +187,7 @@ describe('Call Tests', () => { const localAudioStream = mockStream as unknown as MediaSDK.LocalMicrophoneStream; - const call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId); + const call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId, mockLineId); expect(call).toBeTruthy(); /* After creation , call manager should have 1 record */ @@ -230,7 +232,7 @@ describe('Call Tests', () => { }, }; - const call = callManager.createCall(dest, CallDirection.INBOUND, deviceId); + const call = callManager.createCall(dest, CallDirection.INBOUND, deviceId, mockLineId); const response = await call['postMedia']({}); @@ -238,7 +240,7 @@ describe('Call Tests', () => { }); it('check whether callerId midcall event is serviced or not', async () => { - const call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId); + const call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId, mockLineId); call.handleMidCallEvent(dummyMidCallEvent); await waitForMsecs(50); @@ -248,7 +250,7 @@ describe('Call Tests', () => { }); it('check whether call midcall event is serviced or not', async () => { - const call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId); + const call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId, mockLineId); dummyMidCallEvent.eventType = 'callState'; @@ -265,7 +267,7 @@ describe('Call Tests', () => { }); it('check call stats for active call', async () => { - const call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId); + const call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId, mockLineId); let callRtpStats; @@ -294,6 +296,7 @@ describe('Call Tests', () => { dest, CallDirection.OUTBOUND, deviceId, + mockLineId, deleteCallFromCollection, defaultServiceIndicator ); @@ -333,6 +336,7 @@ describe('Call Tests', () => { dest, CallDirection.OUTBOUND, deviceId, + mockLineId, deleteCallFromCollection, defaultServiceIndicator ); @@ -384,6 +388,7 @@ describe('State Machine handler tests', () => { dest, CallDirection.OUTBOUND, deviceId, + mockLineId, () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const dummy = 10; @@ -1548,6 +1553,7 @@ describe('Supplementary Services tests', () => { dest, CallDirection.OUTBOUND, deviceId, + mockLineId, () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const dummy = 10; @@ -2168,6 +2174,7 @@ describe('Supplementary Services tests', () => { transfereeDest, CallDirection.OUTBOUND, deviceId, + mockLineId, () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const dummy = 10; diff --git a/packages/calling/src/CallingClient/calling/call.ts b/packages/calling/src/CallingClient/calling/call.ts index abfc72be9d0..8bdf4c5d35d 100644 --- a/packages/calling/src/CallingClient/calling/call.ts +++ b/packages/calling/src/CallingClient/calling/call.ts @@ -98,6 +98,8 @@ export class Call extends Eventing implements ICall { private deviceId: string; + public lineId: string; + private disconnectReason: DisconnectReason; private callStateMachine; @@ -188,6 +190,7 @@ export class Call extends Eventing implements ICall { destination: CallDetails, direction: CallDirection, deviceId: string, + lineId: string, deleteCb: DeleteRecordCallBack, indicator: ServiceIndicator ) { @@ -197,6 +200,7 @@ export class Call extends Eventing implements ICall { this.sdkConnector = SDKConnector; this.deviceId = deviceId; this.serviceIndicator = indicator; + this.lineId = lineId; /* istanbul ignore else */ if (!this.sdkConnector.getWebex()) { @@ -2648,6 +2652,7 @@ export class Call extends Eventing implements ICall { * @param dest -. * @param dir -. * @param deviceId -. + * @param lineId -. * @param serverCb * @param deleteCb * @param indicator - Service Indicator. @@ -2658,6 +2663,7 @@ export const createCall = ( dest: CallDetails, dir: CallDirection, deviceId: string, + lineId: string, deleteCb: DeleteRecordCallBack, indicator: ServiceIndicator -): ICall => new Call(activeUrl, webex, dest, dir, deviceId, deleteCb, indicator); +): ICall => new Call(activeUrl, webex, dest, dir, deviceId, lineId, deleteCb, indicator); diff --git a/packages/calling/src/CallingClient/calling/callManager.test.ts b/packages/calling/src/CallingClient/calling/callManager.test.ts index 794df74a6b1..a845293ba20 100644 --- a/packages/calling/src/CallingClient/calling/callManager.test.ts +++ b/packages/calling/src/CallingClient/calling/callManager.test.ts @@ -8,6 +8,7 @@ import {ICall, ICallManager, MobiusCallState} from './types'; import {EVENT_KEYS} from '../../Events/types'; import {Call} from './call'; import log from '../../Logger'; +import {ILine} from '../line/types'; const webex = getTestUtilsWebex(); const defaultServiceIndicator = ServiceIndicator.CALLING; @@ -99,6 +100,11 @@ const successResponseBody = { }, }; +const mockLineId = 'e4e8ee2a-a154-4e52-8f11-ef4cde2dce72'; +const mockLine = { + lineId: mockLineId, +} as ILine; + describe('Call Manager Tests with respect to calls', () => { const dummyResponse = { statusCode: 200, @@ -109,6 +115,7 @@ describe('Call Manager Tests with respect to calls', () => { }, }, }; + const patchMock = jest.spyOn(Call.prototype as any, 'patch'); const setDisconnectReasonMock = jest.spyOn(Call.prototype as any, 'setDisconnectReason'); const deleteCallMock = jest.spyOn(Call.prototype as any, 'delete'); @@ -153,6 +160,7 @@ describe('Call Manager Tests with respect to calls', () => { }); it('create a call using call manager', async () => { + callManager.updateLine('8a67806f-fc4d-446b-a131-31e71ea5b010', mockLine); webex.request.mockReturnValueOnce({ statusCode: 200, body: { @@ -168,20 +176,23 @@ describe('Call Manager Tests with respect to calls', () => { }); expect(callManager).toBeTruthy(); - const call = await callManager.createCall(dest, CallDirection.OUTBOUND, deviceId); + const call = await callManager.createCall(dest, CallDirection.OUTBOUND, deviceId, mockLineId); call.setCallId('8a67806f-fc4d-446b-a131-31e71ea5b020'); expect(call).toBeTruthy(); expect(call.getCallId()).toStrictEqual('8a67806f-fc4d-446b-a131-31e71ea5b020'); + expect(call.lineId).toStrictEqual(mockLineId); }); it('Accept an incoming call from Mobius where Call Setup was the first message', async () => { + callManager.updateLine('375b8503-f716-3407-853b-cd9a8c4419a7', mockLine); callManager.on(EVENT_KEYS.INCOMING_CALL, (callObj: ICall) => { expect(callObj.getCallId()).toStrictEqual(setupEvent.data.callId); expect(callObj.getBroadworksCorrelationInfo()).toStrictEqual( setupEvent.data.broadworksCorrelationInfo ); + expect(callObj.lineId).toStrictEqual(mockLineId); }); patchMock.mockResolvedValue(dummyResponse); @@ -196,6 +207,7 @@ describe('Call Manager Tests with respect to calls', () => { }); it('Accept an incoming call from Mobius where Media Event was the first message', async () => { + callManager.updateLine('8a67806f-fc4d-446b-a131-31e71ea5b010', mockLine); patchMock.mockResolvedValue(dummyResponse); callManager.on(EVENT_KEYS.INCOMING_CALL, (callObj: ICall) => { expect(callObj.getCallId()).toStrictEqual(setupEvent.data.callId); @@ -213,7 +225,7 @@ describe('Call Manager Tests with respect to calls', () => { webex.request.mockReturnValueOnce(successResponseBody); /* lets add a call to disconnect it later */ - const call = await callManager.createCall(dest, CallDirection.OUTBOUND, deviceId); + const call = await callManager.createCall(dest, CallDirection.OUTBOUND, deviceId, mockLineId); expect(Object.keys(callManager.getActiveCalls()).length).toBe(1); /* clear the last added call */ @@ -233,7 +245,7 @@ describe('Call Manager Tests with respect to calls', () => { webex.request.mockReturnValueOnce(successResponseBody); /* lets add a call to disconnect it later */ - await callManager.createCall(dest, CallDirection.OUTBOUND, deviceId); + await callManager.createCall(dest, CallDirection.OUTBOUND, deviceId, mockLineId); expect(Object.keys(callManager.getActiveCalls()).length).toBe(1); /* clear the last added call */ @@ -250,6 +262,7 @@ describe('Call Manager Tests with respect to calls', () => { }); it('Accept an incoming call but Outgoing patch request fails', async () => { + callManager.updateLine('8a67806f-fc4d-446b-a131-31e71ea5b010', mockLine); /* Intentionally failing the Patch with 503 */ dummyResponse.statusCode = 503; patchMock.mockRejectedValue(dummyResponse); @@ -271,6 +284,7 @@ describe('Call Manager Tests with respect to calls', () => { }); it('Walk through an End to End call', async () => { + callManager.updateLine('8a67806f-fc4d-446b-a131-31e71ea5b010', mockLine); let call: ICall; callManager.on(EVENT_KEYS.INCOMING_CALL, async (callObj: ICall) => { @@ -375,7 +389,7 @@ describe('Coverage for Events listener', () => { beforeEach(() => { callManager = getCallManager(webex, defaultServiceIndicator); callManager.removeAllListeners(EVENT_KEYS.INCOMING_CALL); - call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId); + call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId, mockLineId); call.setCallId(dummyCallId); setupEvent.data.correlationId = call.getCorrelationId(); }); diff --git a/packages/calling/src/CallingClient/calling/callManager.ts b/packages/calling/src/CallingClient/calling/callManager.ts index 2890fbaaebb..d507d7044db 100644 --- a/packages/calling/src/CallingClient/calling/callManager.ts +++ b/packages/calling/src/CallingClient/calling/callManager.ts @@ -16,6 +16,7 @@ import { } from './types'; import {createCall} from './call'; import log from '../../Logger'; +import {ILine} from '../line/types'; let callManager: ICallManager; @@ -33,6 +34,8 @@ export class CallManager extends Eventing implements ICallManage private serviceIndicator: ServiceIndicator; + private lineDict: Record; + /** * @param webex -. * @param indicator - Service Indicator. @@ -44,7 +47,7 @@ export class CallManager extends Eventing implements ICallManage if (!this.sdkConnector.getWebex()) { SDKConnector.setWebex(webex); } - + this.lineDict = {}; this.webex = this.sdkConnector.getWebex(); this.callCollection = {}; this.activeMobiusUrl = ''; @@ -59,7 +62,8 @@ export class CallManager extends Eventing implements ICallManage public createCall = ( destination: CallDetails, direction: CallDirection, - deviceId: string + deviceId: string, + lineId: string ): ICall => { log.log('Creating call object', {}); const newCall = createCall( @@ -68,6 +72,7 @@ export class CallManager extends Eventing implements ICallManage destination, direction, deviceId, + lineId, (correlationId: CorrelationId) => { delete this.callCollection[correlationId]; const activeCalls = Object.keys(this.getActiveCalls()).length; @@ -165,11 +170,12 @@ export class CallManager extends Eventing implements ICallManage /* This means it's a new call ... * Create an incoming call object and add to our records */ - + const lineId = this.getLineId(mobiusEvent.data.deviceId); newCall = this.createCall( {} as CallDetails, CallDirection.INBOUND, - mobiusEvent.data.deviceId + mobiusEvent.data.deviceId, + lineId ); log.log( `New incoming call created with correlationId from Call Setup message: ${newCall.getCorrelationId()}`, @@ -207,7 +213,7 @@ export class CallManager extends Eventing implements ICallManage }); newCall.startCallerIdResolution(mobiusEvent.data.callerId); } - /* Signal CallingClient */ + /* Signal Line */ this.emit(EVENT_KEYS.INCOMING_CALL, newCall); @@ -260,10 +266,12 @@ export class CallManager extends Eventing implements ICallManage } else { /* If Call.Media arrived before Call.Setup , we create the Call Object here */ + const lineId = this.getLineId(mobiusEvent.data.deviceId); activeCall = this.createCall( {} as CallDetails, CallDirection.INBOUND, - mobiusEvent.data.deviceId + mobiusEvent.data.deviceId, + lineId ); log.log( `New incoming call created with correlationId from ROAP Message: ${activeCall.getCorrelationId()}`, @@ -421,6 +429,20 @@ export class CallManager extends Eventing implements ICallManage public getActiveCalls = (): Record => { return this.callCollection; }; + + /** + * Adds line instance to lineDict + */ + public updateLine(deviceId: string, line: ILine) { + this.lineDict[deviceId] = line; + } + + /** + * Retrieves line id + */ + private getLineId(deviceId: string) { + return this.lineDict[deviceId].lineId; + } } /** diff --git a/packages/calling/src/CallingClient/calling/types.ts b/packages/calling/src/CallingClient/calling/types.ts index 519685d9656..2c3c8f6b15c 100644 --- a/packages/calling/src/CallingClient/calling/types.ts +++ b/packages/calling/src/CallingClient/calling/types.ts @@ -4,6 +4,7 @@ import {CallError} from '../../Errors/catalog/CallError'; import {CallDetails, CallDirection, CallId, DisplayInformation} from '../../common/types'; import {Eventing} from '../../Events/impl'; import {CallerIdInfo, CallEvent, CallEventTypes, RoapEvent, RoapMessage} from '../../Events/types'; +import {ILine} from '../line/types'; export enum MobiusEventType { CALL_SETUP = 'mobius.call', @@ -194,6 +195,7 @@ export type CallRtpStats = { }; export interface ICall extends Eventing { + lineId: string; getCallId: () => string; getCorrelationId: () => string; getDirection: () => CallDirection; @@ -229,9 +231,15 @@ export type CallErrorEmitterCallBack = (error: CallError) => void; export type RetryCallBack = (interval: number) => void; export interface ICallManager extends Eventing { - createCall: (destination: CallDetails, direction: CallDirection, deviceId: string) => ICall; + createCall: ( + destination: CallDetails, + direction: CallDirection, + deviceId: string, + lineId: string + ) => ICall; endCall: (callId: CallId) => void; getCall: (callId: CallId) => ICall; updateActiveMobius: (url: string) => void; getActiveCalls: () => Record; + updateLine: (deviceId: string, line: ILine) => void; } diff --git a/packages/calling/src/CallingClient/line/index.ts b/packages/calling/src/CallingClient/line/index.ts index 32cba8bfaf9..61d5a9a5a79 100644 --- a/packages/calling/src/CallingClient/line/index.ts +++ b/packages/calling/src/CallingClient/line/index.ts @@ -1,8 +1,16 @@ import {Mutex} from 'async-mutex'; import {v4 as uuid} from 'uuid'; -import {IDeviceInfo, MobiusDeviceId, MobiusStatus, ServiceIndicator} from '../../common/types'; +import { + CallDetails, + CallDirection, + CorrelationId, + IDeviceInfo, + MobiusDeviceId, + MobiusStatus, + ServiceIndicator, +} from '../../common/types'; import {ILine, LINE_EVENTS, LineEventTypes, LineStatus} from './types'; -import {LINE_FILE} from '../constants'; +import {LINE_FILE, VALID_PHONE} from '../constants'; import log from '../../Logger'; import {IRegistration} from '../registration/types'; import {createRegistration} from '../registration'; @@ -13,6 +21,10 @@ import {LineError} from '../../Errors/catalog/LineError'; import {LOGGER} from '../../Logger/types'; import {validateServiceData} from '../../common'; import SDKConnector from '../../SDKConnector'; +import {EVENT_KEYS} from '../../Events/types'; +import {ICall, ICallManager} from '../calling/types'; +import {getCallManager} from '../calling/callManager'; +import {ERROR_TYPE} from '../../Errors/types'; export default class Line extends Eventing implements ILine { #webex: WebexSDK; @@ -57,6 +69,8 @@ export default class Line extends Eventing implements ILine { public voicePortalExtension?: number; + private callManager: ICallManager; + #primaryMobiusUris: string[]; #backupMobiusUris: string[]; @@ -106,6 +120,10 @@ export default class Line extends Eventing implements ILine { this.registration.setStatus(MobiusStatus.DEFAULT); log.setLogger(logLevel, LINE_FILE); + + this.callManager = getCallManager(this.#webex, serviceData.indicator); + + this.incomingCallListener(); } /** @@ -119,6 +137,9 @@ export default class Line extends Eventing implements ILine { this.registration.setMobiusServers(this.#primaryMobiusUris, this.#backupMobiusUris); await this.registration.triggerRegistration(); }); + if (this.mobiusDeviceId) { + this.callManager.updateLine(this.mobiusDeviceId, this); + } } /** @@ -204,4 +225,71 @@ export default class Line extends Eventing implements ILine { */ public getDeviceId = (): MobiusDeviceId | undefined => this.registration.getDeviceInfo().device?.deviceId; + + /** + * @param dest -. + */ + public makeCall = (dest: CallDetails): ICall | undefined => { + let call; + + if (dest) { + const match = dest.address.match(VALID_PHONE); + + if (match && match[0].length === dest.address.length) { + const sanitizedNumber = dest.address + .replace(/[^[*+]\d#]/gi, '') + .replace(/\s+/gi, '') + .replace(/-/gi, ''); + const formattedDest = { + type: dest.type, + address: `tel:${sanitizedNumber}`, + }; + + call = this.callManager.createCall( + formattedDest, + CallDirection.OUTBOUND, + this.registration.getDeviceInfo().device?.deviceId as string, + this.lineId + ); + log.log(`New call created, callId: ${call.getCallId()}`, {}); + } else { + log.warn('Invalid phone number detected', {}); + + const err = new LineError( + 'An invalid phone number was detected. Check the number and try again.', + {}, + ERROR_TYPE.CALL_ERROR, + LineStatus.ACTIVE + ); + + this.emit(LINE_EVENTS.ERROR, err); + } + + return call; + } + + return undefined; + }; + + /** + * An Incoming Call listener. + */ + private incomingCallListener() { + const logContext = { + file: LINE_FILE, + method: this.incomingCallListener.name, + }; + log.log('Listening for incoming calls... ', logContext); + this.callManager.on(EVENT_KEYS.INCOMING_CALL, (callObj: ICall) => { + this.emit(LINE_EVENTS.INCOMING_CALL, callObj); + }); + } + + /** + * @param callId -. + * @param correlationId -. + */ + public getCall = (correlationId: CorrelationId): ICall => { + return this.callManager.getCall(correlationId); + }; } diff --git a/packages/calling/src/CallingClient/line/line.test.ts b/packages/calling/src/CallingClient/line/line.test.ts index 149cfd26141..bef346e3485 100644 --- a/packages/calling/src/CallingClient/line/line.test.ts +++ b/packages/calling/src/CallingClient/line/line.test.ts @@ -8,6 +8,8 @@ import { import {URL} from '../registration/registerFixtures'; import {filterMobiusUris} from '../../common'; import { + CallDirection, + CallType, MobiusServers, MobiusStatus, ServiceIndicator, @@ -20,6 +22,7 @@ import SDKConnector from '../../SDKConnector'; import {REGISTRATION_FILE} from '../constants'; import {LOGGER} from '../../Logger/types'; import * as regUtils from '../registration/register'; +import {EVENT_KEYS} from '../../Events/types'; describe('Line Tests', () => { const mutex = new Mutex(); @@ -154,4 +157,132 @@ describe('Line Tests', () => { expect(line.getRegistrationStatus()).toEqual(MobiusStatus.DEFAULT); }); }); + describe('Line calling tests', () => { + let line; + + beforeEach(() => { + line = new Line( + userId, + clientDeviceUri, + LineStatus.ACTIVE, + mutex, + primaryMobiusUris(), + backupMobiusUris(), + LOGGER.INFO + ); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.clearAllMocks(); + jest.useRealTimers(); + line.removeAllListeners(); + }); + it('Return a successful call object while making call', () => { + const createCallSpy = jest.spyOn(line.callManager, 'createCall'); + const call = line.makeCall({address: '5003', type: CallType.URI}); + + expect(createCallSpy).toBeCalledOnceWith( + {address: 'tel:5003', type: 'uri'}, + CallDirection.OUTBOUND, + undefined, + line.lineId + ); + expect(call).toBeTruthy(); + expect(line.getCall(call ? call.getCorrelationId() : '')).toBe(call); + expect(call ? call.direction : undefined).toStrictEqual(CallDirection.OUTBOUND); + call?.end(); + }); + + it('Return a successful call object while making call to FAC codes', () => { + const createCallSpy = jest.spyOn(line.callManager, 'createCall'); + const call = line.makeCall({address: '*25', type: CallType.URI}); + + expect(createCallSpy).toBeCalledOnceWith( + {address: 'tel:*25', type: 'uri'}, + CallDirection.OUTBOUND, + undefined, + line.lineId + ); + expect(call).toBeTruthy(); + expect(call ? call.direction : undefined).toStrictEqual(CallDirection.OUTBOUND); + call?.end(); + }); + + it('Remove spaces from dialled number while making call', () => { + const createCallSpy = jest.spyOn(line.callManager, 'createCall'); + const call = line.makeCall({address: '+91 123 456 7890', type: CallType.URI}); + + expect(createCallSpy).toBeCalledOnceWith( + {address: 'tel:+911234567890', type: 'uri'}, + CallDirection.OUTBOUND, + undefined, + line.lineId + ); + expect(call).toBeTruthy(); + expect(call ? call.direction : undefined).toStrictEqual(CallDirection.OUTBOUND); + expect(call ? call.destination.address : undefined).toStrictEqual('tel:+911234567890'); + call?.end(); + }); + + it('Remove hyphen from dialled number while making call', () => { + const createCallSpy = jest.spyOn(line.callManager, 'createCall'); + const call = line.makeCall({address: '123-456-7890', type: CallType.URI}); + + expect(createCallSpy).toBeCalledOnceWith( + {address: 'tel:1234567890', type: 'uri'}, + CallDirection.OUTBOUND, + undefined, + line.lineId + ); + expect(call).toBeTruthy(); + expect(call ? call.direction : undefined).toStrictEqual(CallDirection.OUTBOUND); + expect(call ? call.destination.address : undefined).toStrictEqual('tel:1234567890'); + call?.end(); + }); + + it('attempt to create call with incorrect number format 1', (done) => { + // There may be other listeners , which may create race + line.removeAllListeners(LINE_EVENTS.ERROR); + const createCallSpy = jest.spyOn(line.callManager, 'createCall'); + + line.on(LINE_EVENTS.ERROR, (error) => { + expect(error.message).toBe( + 'An invalid phone number was detected. Check the number and try again.' + ); + done(); + }); + try { + const call = line.makeCall({address: 'select#$@^^', type: CallType.URI}); + + expect(call).toBeUndefined(); + expect(createCallSpy).not.toBeCalledOnceWith({}); + } catch (error) { + done(error); + } + }); + + it('attempt to create call with incorrect number format 2', (done) => { + expect.assertions(4); + // There may be other listeners , which may create race + line.removeAllListeners(LINE_EVENTS.ERROR); + const createCallSpy = jest.spyOn(line.callManager, 'createCall'); + + line.on(LINE_EVENTS.ERROR, (error) => { + expect(error.message).toBe( + 'An invalid phone number was detected. Check the number and try again.' + ); + done(); + }); + + try { + const call = line.makeCall({address: '+1@8883332505', type: CallType.URI}); + + expect(call).toBeUndefined(); + expect(createCallSpy).not.toBeCalledOnceWith({}); + } catch (error) { + done(error); + } + }); + }); }); diff --git a/packages/calling/src/CallingClient/line/types.ts b/packages/calling/src/CallingClient/line/types.ts index d607df5999b..eacda478f55 100644 --- a/packages/calling/src/CallingClient/line/types.ts +++ b/packages/calling/src/CallingClient/line/types.ts @@ -1,6 +1,13 @@ import {IRegistration} from '../registration/types'; import {LineError} from '../../Errors/catalog/LineError'; -import {IDeviceInfo, MobiusDeviceId, MobiusStatus} from '../../common/types'; +import { + CallDetails, + CorrelationId, + IDeviceInfo, + MobiusDeviceId, + MobiusStatus, +} from '../../common/types'; +import {ICall} from '../calling/types'; export enum LineStatus { INACTIVE = 'inactive', @@ -14,6 +21,7 @@ export enum LINE_EVENTS { RECONNECTING = 'reconnecting', REGISTERED = 'registered', UNREGISTERED = 'unregistered', + INCOMING_CALL = 'line:incoming_call', } export interface ILine { @@ -40,6 +48,8 @@ export interface ILine { getRegistrationStatus: () => MobiusStatus; getDeviceId: () => MobiusDeviceId | undefined; lineEmitter: (event: LINE_EVENTS, deviceInfo?: IDeviceInfo, lineError?: LineError) => void; + makeCall: (dest: CallDetails) => ICall | undefined; + getCall: (correlationId: CorrelationId) => ICall; } export type LineEventTypes = { @@ -49,6 +59,7 @@ export type LineEventTypes = { [LINE_EVENTS.RECONNECTING]: () => void; [LINE_EVENTS.REGISTERED]: (lineInfo: ILine) => void; [LINE_EVENTS.UNREGISTERED]: () => void; + [LINE_EVENTS.INCOMING_CALL]: (callObj: ICall) => void; }; export type LineEmitterCallback = ( diff --git a/packages/calling/src/CallingClient/types.ts b/packages/calling/src/CallingClient/types.ts index fe7ed9ea255..1336f1b5439 100644 --- a/packages/calling/src/CallingClient/types.ts +++ b/packages/calling/src/CallingClient/types.ts @@ -31,6 +31,6 @@ export interface ICallingClient extends Eventing { mediaEngine: typeof Media; getSDKConnector: () => ISDKConnector; getLoggingLevel: () => LOGGER; - makeCall: (dest: CallDetails) => ICall | undefined; - getCall: (correlationId: CorrelationId) => ICall; + getActiveCalls: () => Record; + getConnectedCall: () => ICall | undefined; } diff --git a/packages/calling/src/Events/types.ts b/packages/calling/src/Events/types.ts index 855b6b3a80a..d1b7d2249b7 100644 --- a/packages/calling/src/Events/types.ts +++ b/packages/calling/src/Events/types.ts @@ -5,22 +5,22 @@ import {CallError, CallingClientError} from '../Errors'; /** External Eventing Start */ export enum EVENT_KEYS { - ALERTING = 'call:alerting', + ALERTING = 'alerting', CALL_ERROR = 'call:error', - CALLER_ID = 'call:caller_id', - CONNECT = 'call:connect', - DISCONNECT = 'call:disconnect', + CALLER_ID = 'caller_id', + CONNECT = 'connect', + DISCONNECT = 'disconnect', ERROR = 'callingClient:error', - ESTABLISHED = 'call:established', - HELD = 'call:held', - HOLD_ERROR = 'call:hold_error', - INCOMING_CALL = 'callingClient:incoming_call', + ESTABLISHED = 'established', + HELD = 'held', + HOLD_ERROR = 'hold_error', + INCOMING_CALL = 'line:incoming_call', OUTGOING_CALL = 'callingClient:outgoing_call', - PROGRESS = 'call:progress', - REMOTE_MEDIA = 'call:remote_media', - RESUME_ERROR = 'call:resume_error', - RESUMED = 'call:resumed', - TRANSFER_ERROR = 'call:transfer_error', + PROGRESS = 'progress', + REMOTE_MEDIA = 'remote_media', + RESUME_ERROR = 'resume_error', + RESUMED = 'resumed', + TRANSFER_ERROR = 'transfer_error', USER_SESSION_INFO = 'callingClient:user_recent_sessions', CB_VOICEMESSAGE_CONTENT_GET = 'call_back_voicemail_content_get', CALL_HISTORY_USER_SESSION_INFO = 'callHistory:user_recent_sessions', @@ -188,7 +188,6 @@ export type VoicemailEventTypes = { export type CallingClientEventTypes = { [EVENT_KEYS.ERROR]: (error: CallingClientError) => void; [EVENT_KEYS.USER_SESSION_INFO]: (event: CallSessionEvent) => void; - [EVENT_KEYS.INCOMING_CALL]: (callObj: ICall) => void; [EVENT_KEYS.OUTGOING_CALL]: (callId: string) => void; [EVENT_KEYS.ALL_CALLS_CLEARED]: () => void; };