Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Fix virtual / native room mapping on call transfers #7848

Merged
merged 3 commits into from
Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/CallHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -901,7 +901,7 @@ export default class CallHandler extends EventEmitter {
this.pause(AudioID.Ring);
}

public async dialNumber(number: string): Promise<void> {
public async dialNumber(number: string, transferee?: MatrixCall): Promise<void> {
const results = await this.pstnLookup(number);
if (!results || results.length === 0 || !results[0].userid) {
Modal.createTrackedDialog('', '', ErrorDialog, {
Expand Down Expand Up @@ -932,12 +932,19 @@ export default class CallHandler extends EventEmitter {
metricsTrigger: "WebDialPad",
});

await this.placeMatrixCall(roomId, CallType.Voice, null);
await this.placeMatrixCall(roomId, CallType.Voice, transferee);
}

public async startTransferToPhoneNumber(
call: MatrixCall, destination: string, consultFirst: boolean,
): Promise<void> {
if (consultFirst) {
// if we're consulting, we just start by placing a call to the transfer
// target (passing the transferee so the actual tranfer can happen later)
this.dialNumber(destination, call);
return;
}

const results = await this.pstnLookup(destination);
if (!results || results.length === 0 || !results[0].userid) {
Modal.createTrackedDialog('', '', ErrorDialog, {
Expand Down
186 changes: 141 additions & 45 deletions test/CallHandler-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ limitations under the License.

import './skinned-sdk';

import { IProtocol } from 'matrix-js-sdk/src';
import { CallEvent, CallState, CallType } from 'matrix-js-sdk/src/webrtc/call';
import EventEmitter from 'events';

import CallHandler, { CallHandlerEvent } from '../src/CallHandler';
import CallHandler, {
CallHandlerEvent, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, PROTOCOL_SIP_NATIVE, PROTOCOL_SIP_VIRTUAL,
} from '../src/CallHandler';
import { stubClient, mkStubRoom } from './test-utils';
import { MatrixClientPeg } from '../src/MatrixClientPeg';
import dis from '../src/dispatcher/dispatcher';
Expand All @@ -28,9 +31,26 @@ import SdkConfig from '../src/SdkConfig';
import { ActionPayload } from '../src/dispatcher/payloads';
import { Action } from "../src/dispatcher/actions";

const REAL_ROOM_ID = '$room1:example.org';
const MAPPED_ROOM_ID = '$room2:example.org';
const MAPPED_ROOM_ID_2 = '$room3:example.org';
// The Matrix IDs that the user sees when talking to Alice & Bob
const NATIVE_ALICE = "@alice:example.org";
const NATIVE_BOB = "@bob:example.org";
const NATIVE_CHARLIE = "@charlie:example.org";

// Virtual user for Bob
const VIRTUAL_BOB = "@virtual_bob:example.org";

//const REAL_ROOM_ID = "$room1:example.org";
// The rooms the user sees when they're communicating with these users
const NATIVE_ROOM_ALICE = "$alice_room:example.org";
const NATIVE_ROOM_BOB = "$bob_room:example.org";
const NATIVE_ROOM_CHARLIE = "$charlie_room:example.org";

// The room we use to talk to virtual Bob (but that the user does not see)
// Bob has a virtual room, but Alice doesn't
const VIRTUAL_ROOM_BOB = "$virtual_bob_room:example.org";

// Bob's phone number
const BOB_PHONE_NUMBER = "01818118181";

function mkStubDM(roomId, userId) {
const room = mkStubRoom(roomId);
Expand Down Expand Up @@ -103,6 +123,9 @@ describe('CallHandler', () => {
let audioElement;
let fakeCall;

let pstnLookup;
let nativeLookup;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these need types


beforeEach(() => {
stubClient();
MatrixClientPeg.get().createCall = roomId => {
Expand All @@ -113,48 +136,107 @@ describe('CallHandler', () => {
return fakeCall;
};

MatrixClientPeg.get().getThirdpartyProtocols = () => {
return Promise.resolve({
"m.id.phone": {} as IProtocol,
"im.vector.protocol.sip_native": {} as IProtocol,
"im.vector.protocol.sip_virtual": {} as IProtocol,
});
};

callHandler = new CallHandler();
callHandler.start();

const realRoom = mkStubDM(REAL_ROOM_ID, '@user1:example.org');
const mappedRoom = mkStubDM(MAPPED_ROOM_ID, '@user2:example.org');
const mappedRoom2 = mkStubDM(MAPPED_ROOM_ID_2, '@user3:example.org');
const nativeRoomAlice = mkStubDM(NATIVE_ROOM_ALICE, NATIVE_ALICE);
const nativeRoomBob = mkStubDM(NATIVE_ROOM_BOB, NATIVE_BOB);
const nativeRoomCharie = mkStubDM(NATIVE_ROOM_CHARLIE, NATIVE_CHARLIE);
const virtualBobRoom = mkStubDM(VIRTUAL_ROOM_BOB, VIRTUAL_BOB);

MatrixClientPeg.get().getRoom = roomId => {
switch (roomId) {
case REAL_ROOM_ID:
return realRoom;
case MAPPED_ROOM_ID:
return mappedRoom;
case MAPPED_ROOM_ID_2:
return mappedRoom2;
case NATIVE_ROOM_ALICE:
return nativeRoomAlice;
case NATIVE_ROOM_BOB:
return nativeRoomBob;
case NATIVE_ROOM_CHARLIE:
return nativeRoomCharie;
case VIRTUAL_ROOM_BOB:
return virtualBobRoom;
}
};

dmRoomMap = {
getUserIdForRoomId: roomId => {
if (roomId === REAL_ROOM_ID) {
return '@user1:example.org';
} else if (roomId === MAPPED_ROOM_ID) {
return '@user2:example.org';
} else if (roomId === MAPPED_ROOM_ID_2) {
return '@user3:example.org';
if (roomId === NATIVE_ROOM_ALICE) {
return NATIVE_ALICE;
} else if (roomId === NATIVE_ROOM_BOB) {
return NATIVE_BOB;
} else if (roomId === NATIVE_ROOM_CHARLIE) {
return NATIVE_CHARLIE;
} else if (roomId === VIRTUAL_ROOM_BOB) {
return VIRTUAL_BOB;
} else {
return null;
}
},
getDMRoomsForUserId: userId => {
if (userId === '@user2:example.org') {
return [MAPPED_ROOM_ID];
} else if (userId === '@user3:example.org') {
return [MAPPED_ROOM_ID_2];
if (userId === NATIVE_ALICE) {
return [NATIVE_ROOM_ALICE];
} else if (userId === NATIVE_BOB) {
return [NATIVE_ROOM_BOB];
} else if (userId === NATIVE_CHARLIE) {
return [NATIVE_ROOM_CHARLIE];
} else if (userId === VIRTUAL_BOB) {
return [VIRTUAL_ROOM_BOB];
} else {
return [];
}
},
};
DMRoomMap.setShared(dmRoomMap);

pstnLookup = null;
nativeLookup = null;

MatrixClientPeg.get().getThirdpartyUser = (proto, params) => {
if ([PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED].includes(proto)) {
pstnLookup = params['m.id.phone'];
return Promise.resolve([{
userid: VIRTUAL_BOB,
protocol: "m.id.phone",
fields: {
is_native: true,
lookup_success: true,
},
}]);
} else if (proto === PROTOCOL_SIP_NATIVE) {
nativeLookup = params['virtual_mxid'];
if (params['virtual_mxid'] === VIRTUAL_BOB) {
return Promise.resolve([{
userid: NATIVE_BOB,
protocol: "im.vector.protocol.sip_native",
fields: {
is_native: true,
lookup_success: true,
},
}]);
}
return Promise.resolve([]);
} else if (proto === PROTOCOL_SIP_VIRTUAL) {
if (params['native_mxid'] === NATIVE_BOB) {
return Promise.resolve([{
userid: VIRTUAL_BOB,
protocol: "im.vector.protocol.sip_virtual",
fields: {
is_virtual: true,
lookup_success: true,
},
}]);
}
return Promise.resolve([]);
}
};

audioElement = document.createElement('audio');
audioElement.id = "remoteAudio";
document.body.appendChild(audioElement);
Expand All @@ -173,31 +255,45 @@ describe('CallHandler', () => {
});

it('should look up the correct user and start a call in the room when a phone number is dialled', async () => {
MatrixClientPeg.get().getThirdpartyUser = jest.fn().mockResolvedValue([{
userid: '@user2:example.org',
protocol: "im.vector.protocol.sip_native",
fields: {
is_native: true,
lookup_success: true,
},
}]);
await callHandler.dialNumber(BOB_PHONE_NUMBER);

expect(pstnLookup).toEqual(BOB_PHONE_NUMBER);
expect(nativeLookup).toEqual(VIRTUAL_BOB);

// we should have switched to the native room for Bob
const viewRoomPayload = await untilDispatch(Action.ViewRoom);
expect(viewRoomPayload.room_id).toEqual(NATIVE_ROOM_BOB);

// Check that a call was started: its room on the protocol level
// should be the virtual room
expect(fakeCall.roomId).toEqual(VIRTUAL_ROOM_BOB);

await callHandler.dialNumber('01818118181');
// but it should appear to the user to be in thw native room for Bob
expect(callHandler.roomIdForCall(fakeCall)).toEqual(NATIVE_ROOM_BOB);
});

it('should look up the correct user and start a call in the room when a call is transferred', async () => {
// we can pass a very minimal object as as the call since we pass consultFirst=true:
// we don't need to actually do any transferring
const mockTransferreeCall = { type: CallType.Voice };
await callHandler.startTransferToPhoneNumber(mockTransferreeCall, BOB_PHONE_NUMBER, true);

// same checks as above
const viewRoomPayload = await untilDispatch(Action.ViewRoom);
expect(viewRoomPayload.room_id).toEqual(MAPPED_ROOM_ID);
expect(viewRoomPayload.room_id).toEqual(NATIVE_ROOM_BOB);

expect(fakeCall.roomId).toEqual(VIRTUAL_ROOM_BOB);

// Check that a call was started
expect(fakeCall.roomId).toEqual(MAPPED_ROOM_ID);
expect(callHandler.roomIdForCall(fakeCall)).toEqual(NATIVE_ROOM_BOB);
});

it('should move calls between rooms when remote asserted identity changes', async () => {
callHandler.placeCall(REAL_ROOM_ID, CallType.Voice);
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);

await untilCallHandlerEvent(callHandler, CallHandlerEvent.CallState);

// should start off in the actual room ID it's in at the protocol level
expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBe(fakeCall);
// We placed the call in Alice's room so it should start off there
expect(callHandler.getCallForRoom(NATIVE_ROOM_ALICE)).toBe(fakeCall);

let callRoomChangeEventCount = 0;
const roomChangePromise = new Promise<void>(resolve => {
Expand All @@ -207,10 +303,10 @@ describe('CallHandler', () => {
});
});

// Now emit an asserted identity for user2: this should be ignored
// Now emit an asserted identity for Bob: this should be ignored
// because we haven't set the config option to obey asserted identity
fakeCall.getRemoteAssertedIdentity = jest.fn().mockReturnValue({
id: "@user2:example.org",
id: NATIVE_BOB,
});
fakeCall.emit(CallEvent.AssertedIdentityChanged);

Expand All @@ -223,21 +319,21 @@ describe('CallHandler', () => {

// ...and send another asserted identity event for a different user
fakeCall.getRemoteAssertedIdentity = jest.fn().mockReturnValue({
id: "@user3:example.org",
id: NATIVE_CHARLIE,
});
fakeCall.emit(CallEvent.AssertedIdentityChanged);

await roomChangePromise;
callHandler.removeAllListeners();

// If everything's gone well, we should have seen only one room change
// event and the call should now be in user 3's room.
// If it's not obeying any, the call will still be in REAL_ROOM_ID.
// event and the call should now be in Charlie's room.
// If it's not obeying any, the call will still be in NATIVE_ROOM_ALICE.
// If it incorrectly obeyed both asserted identity changes, either it will
// have just processed one and the call will be in the wrong room, or we'll
// have seen two room change dispatches.
expect(callRoomChangeEventCount).toEqual(1);
expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBeNull();
expect(callHandler.getCallForRoom(MAPPED_ROOM_ID_2)).toBe(fakeCall);
expect(callHandler.getCallForRoom(NATIVE_ROOM_BOB)).toBeNull();
expect(callHandler.getCallForRoom(NATIVE_ROOM_CHARLIE)).toBe(fakeCall);
});
});