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

Commit f703383

Browse files
authored
Merge pull request #5560 from matrix-org/dbkr/voip_user_mapper
Add VoIP user mapper
2 parents bb50cd5 + 3803a5e commit f703383

File tree

7 files changed

+172
-33
lines changed

7 files changed

+172
-33
lines changed

src/CallHandler.tsx

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import {UIFeature} from "./settings/UIFeature";
8383
import { CallError } from "matrix-js-sdk/src/webrtc/call";
8484
import { logger } from 'matrix-js-sdk/src/logger';
8585
import { Action } from './dispatcher/actions';
86+
import { roomForVirtualRoom, getOrCreateVirtualRoomForRoom } from './VoipUserMapper';
8687

8788
const CHECK_PSTN_SUPPORT_ATTEMPTS = 3;
8889

@@ -133,6 +134,15 @@ export default class CallHandler {
133134
return window.mxCallHandler;
134135
}
135136

137+
/*
138+
* Gets the user-facing room associated with a call (call.roomId may be the call "virtual room"
139+
* if a voip_mxid_translate_pattern is set in the config)
140+
*/
141+
public static roomIdForCall(call: MatrixCall) {
142+
if (!call) return null;
143+
return roomForVirtualRoom(call.roomId) || call.roomId;
144+
}
145+
136146
start() {
137147
this.dispatcherRef = dis.register(this.onAction);
138148
// add empty handlers for media actions, otherwise the media keys
@@ -284,11 +294,15 @@ export default class CallHandler {
284294
// We don't allow placing more than one call per room, but that doesn't mean there
285295
// can't be more than one, eg. in a glare situation. This checks that the given call
286296
// is the call we consider 'the' call for its room.
287-
const callForThisRoom = this.getCallForRoom(call.roomId);
297+
const mappedRoomId = CallHandler.roomIdForCall(call);
298+
299+
const callForThisRoom = this.getCallForRoom(mappedRoomId);
288300
return callForThisRoom && call.callId === callForThisRoom.callId;
289301
}
290302

291303
private setCallListeners(call: MatrixCall) {
304+
const mappedRoomId = CallHandler.roomIdForCall(call);
305+
292306
call.on(CallEvent.Error, (err: CallError) => {
293307
if (!this.matchesCallForThisRoom(call)) return;
294308

@@ -318,7 +332,7 @@ export default class CallHandler {
318332

319333
Analytics.trackEvent('voip', 'callHangup');
320334

321-
this.removeCallForRoom(call.roomId);
335+
this.removeCallForRoom(mappedRoomId);
322336
});
323337
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
324338
if (!this.matchesCallForThisRoom(call)) return;
@@ -343,7 +357,7 @@ export default class CallHandler {
343357
break;
344358
case CallState.Ended:
345359
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
346-
this.removeCallForRoom(call.roomId);
360+
this.removeCallForRoom(mappedRoomId);
347361
if (oldState === CallState.InviteSent && (
348362
call.hangupParty === CallParty.Remote ||
349363
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
@@ -392,7 +406,7 @@ export default class CallHandler {
392406
this.pause(AudioID.Ringback);
393407
}
394408

395-
this.calls.set(newCall.roomId, newCall);
409+
this.calls.set(mappedRoomId, newCall);
396410
this.setCallListeners(newCall);
397411
this.setCallState(newCall, newCall.state);
398412
});
@@ -404,13 +418,15 @@ export default class CallHandler {
404418
}
405419

406420
private setCallState(call: MatrixCall, status: CallState) {
421+
const mappedRoomId = CallHandler.roomIdForCall(call);
422+
407423
console.log(
408-
`Call state in ${call.roomId} changed to ${status}`,
424+
`Call state in ${mappedRoomId} changed to ${status}`,
409425
);
410426

411427
dis.dispatch({
412428
action: 'call_state',
413-
room_id: call.roomId,
429+
room_id: mappedRoomId,
414430
state: status,
415431
});
416432
}
@@ -477,14 +493,20 @@ export default class CallHandler {
477493
}, null, true);
478494
}
479495

480-
private placeCall(
496+
private async placeCall(
481497
roomId: string, type: PlaceCallType,
482498
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
483499
) {
484500
Analytics.trackEvent('voip', 'placeCall', 'type', type);
485501
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
486-
const call = createNewMatrixCall(MatrixClientPeg.get(), roomId);
502+
503+
const mappedRoomId = (await getOrCreateVirtualRoomForRoom(roomId)) || roomId;
504+
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
505+
506+
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
507+
487508
this.calls.set(roomId, call);
509+
488510
this.setCallListeners(call);
489511
this.setCallAudioElement(call);
490512

@@ -586,13 +608,14 @@ export default class CallHandler {
586608

587609
const call = payload.call as MatrixCall;
588610

589-
if (this.getCallForRoom(call.roomId)) {
611+
const mappedRoomId = CallHandler.roomIdForCall(call);
612+
if (this.getCallForRoom(mappedRoomId)) {
590613
// ignore multiple incoming calls to the same room
591614
return;
592615
}
593616

594617
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
595-
this.calls.set(call.roomId, call)
618+
this.calls.set(mappedRoomId, call)
596619
this.setCallListeners(call);
597620
}
598621
break;

src/VoipUserMapper.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { ensureDMExists, findDMForUser } from './createRoom';
18+
import { MatrixClientPeg } from "./MatrixClientPeg";
19+
import DMRoomMap from "./utils/DMRoomMap";
20+
import SdkConfig from "./SdkConfig";
21+
22+
// Functions for mapping users & rooms for the voip_mxid_translate_pattern
23+
// config option
24+
25+
export function voipUserMapperEnabled(): boolean {
26+
return SdkConfig.get()['voip_mxid_translate_pattern'] !== undefined;
27+
}
28+
29+
// only exported for tests
30+
export function userToVirtualUser(userId: string, templateString?: string): string {
31+
if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern'];
32+
if (!templateString) return null;
33+
return templateString.replace('${mxid}', encodeURIComponent(userId).replace(/%/g, '=').toLowerCase());
34+
}
35+
36+
// only exported for tests
37+
export function virtualUserToUser(userId: string, templateString?: string): string {
38+
if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern'];
39+
if (!templateString) return null;
40+
41+
const regexString = templateString.replace('${mxid}', '(.+)');
42+
43+
const match = userId.match('^' + regexString + '$');
44+
if (!match) return null;
45+
46+
return decodeURIComponent(match[1].replace(/=/g, '%'));
47+
}
48+
49+
async function getOrCreateVirtualRoomForUser(userId: string):Promise<string> {
50+
const virtualUser = userToVirtualUser(userId);
51+
if (!virtualUser) return null;
52+
53+
return await ensureDMExists(MatrixClientPeg.get(), virtualUser);
54+
}
55+
56+
export async function getOrCreateVirtualRoomForRoom(roomId: string):Promise<string> {
57+
const user = DMRoomMap.shared().getUserIdForRoomId(roomId);
58+
if (!user) return null;
59+
return getOrCreateVirtualRoomForUser(user);
60+
}
61+
62+
export function roomForVirtualRoom(roomId: string):string {
63+
const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId);
64+
if (!virtualUser) return null;
65+
const realUser = virtualUserToUser(virtualUser);
66+
const room = findDMForUser(MatrixClientPeg.get(), realUser);
67+
if (room) {
68+
return room.roomId;
69+
} else {
70+
return null;
71+
}
72+
}
73+
74+
export function isVirtualRoom(roomId: string):boolean {
75+
const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId);
76+
if (!virtualUser) return null;
77+
const realUser = virtualUserToUser(virtualUser);
78+
return Boolean(realUser);
79+
}

src/components/views/rooms/MessageComposer.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,12 @@ function HangupButton(props) {
109109

110110
dis.dispatch({
111111
action,
112-
// hangup the call for this room, which may not be the room in props
113-
// (e.g. conferences which will hangup the 1:1 room instead)
114-
room_id: call.roomId,
112+
// hangup the call for this room. NB. We use the room in props as the room ID
113+
// as call.roomId may be the 'virtual room', and the dispatch actions always
114+
// use the user-facing room (there was a time when we deliberately used
115+
// call.roomId and *not* props.roomId, but that was for the old
116+
// style Freeswitch conference calls and those times are gone.)
117+
room_id: props.roomId,
115118
});
116119
};
117120

src/components/views/voip/CallView.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,10 @@ export default class CallView extends React.Component<IProps, IState> {
212212
};
213213

214214
private onExpandClick = () => {
215+
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
215216
dis.dispatch({
216217
action: 'view_room',
217-
room_id: this.props.call.roomId,
218+
room_id: userFacingRoomId,
218219
});
219220
};
220221

@@ -340,27 +341,33 @@ export default class CallView extends React.Component<IProps, IState> {
340341
};
341342

342343
private onRoomAvatarClick = () => {
344+
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
343345
dis.dispatch({
344346
action: 'view_room',
345-
room_id: this.props.call.roomId,
347+
room_id: userFacingRoomId,
346348
});
347349
}
348350

349351
private onSecondaryRoomAvatarClick = () => {
352+
const userFacingRoomId = CallHandler.roomIdForCall(this.props.secondaryCall);
353+
350354
dis.dispatch({
351355
action: 'view_room',
352-
room_id: this.props.secondaryCall.roomId,
356+
room_id: userFacingRoomId,
353357
});
354358
}
355359

356360
private onCallResumeClick = () => {
357-
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
361+
const userFacingRoomId = CallHandler.roomIdForCall(this.props.call);
362+
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
358363
}
359364

360365
public render() {
361366
const client = MatrixClientPeg.get();
362-
const callRoom = client.getRoom(this.props.call.roomId);
363-
const secCallRoom = this.props.secondaryCall ? client.getRoom(this.props.secondaryCall.roomId) : null;
367+
const callRoomId = CallHandler.roomIdForCall(this.props.call);
368+
const secondaryCallRoomId = CallHandler.roomIdForCall(this.props.secondaryCall);
369+
const callRoom = client.getRoom(callRoomId);
370+
const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
364371

365372
let dialPad;
366373
let contextMenu;
@@ -456,7 +463,7 @@ export default class CallView extends React.Component<IProps, IState> {
456463
onClick={() => {
457464
dis.dispatch({
458465
action: 'hangup',
459-
room_id: this.props.call.roomId,
466+
room_id: callRoomId,
460467
});
461468
}}
462469
/>

src/components/views/voip/IncomingCallBox.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,15 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
7070
e.stopPropagation();
7171
dis.dispatch({
7272
action: 'answer',
73-
room_id: this.state.incomingCall.roomId,
73+
room_id: CallHandler.roomIdForCall(this.state.incomingCall),
7474
});
7575
};
7676

7777
private onRejectClick: React.MouseEventHandler = (e) => {
7878
e.stopPropagation();
7979
dis.dispatch({
8080
action: 'reject',
81-
room_id: this.state.incomingCall.roomId,
81+
room_id: CallHandler.roomIdForCall(this.state.incomingCall),
8282
});
8383
};
8484

@@ -89,7 +89,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
8989

9090
let room = null;
9191
if (this.state.incomingCall) {
92-
room = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId);
92+
room = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.state.incomingCall));
9393
}
9494

9595
const caller = room ? room.name : _t("Unknown caller");

src/stores/room-list/filters/VisibilityProvider.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import {Room} from "matrix-js-sdk/src/models/room";
1818
import { RoomListCustomisations } from "../../../customisations/RoomList";
19+
import { isVirtualRoom, voipUserMapperEnabled } from "../../../VoipUserMapper";
1920

2021
export class VisibilityProvider {
2122
private static internalInstance: VisibilityProvider;
@@ -31,18 +32,13 @@ export class VisibilityProvider {
3132
}
3233

3334
public isRoomVisible(room: Room): boolean {
34-
/* eslint-disable prefer-const */
3535
let isVisible = true; // Returned at the end of this function
3636
let forced = false; // When true, this function won't bother calling the customisation points
37-
/* eslint-enable prefer-const */
38-
39-
// ------
40-
// TODO: The `if` statements to control visibility of custom room types
41-
// would go here. The remainder of this function assumes that the statements
42-
// will be here.
43-
//
44-
// When removing this comment block, please remove the lint disable lines in the area.
45-
// ------
37+
38+
if (voipUserMapperEnabled() && isVirtualRoom(room.roomId)) {
39+
isVisible = false;
40+
forced = true;
41+
}
4642

4743
const isVisibleFn = RoomListCustomisations.isRoomVisible;
4844
if (!forced && isVisibleFn) {

test/VoipUserMapper-test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { userToVirtualUser, virtualUserToUser } from '../src/VoipUserMapper';
18+
19+
const templateString = '@_greatappservice_${mxid}:frooble.example';
20+
const realUser = '@alice:boop.example';
21+
const virtualUser = "@_greatappservice_=40alice=3aboop.example:frooble.example";
22+
23+
describe('VoipUserMapper', function() {
24+
it('translates users to virtual users', function() {
25+
expect(userToVirtualUser(realUser, templateString)).toEqual(virtualUser);
26+
});
27+
28+
it('translates users to virtual users', function() {
29+
expect(virtualUserToUser(virtualUser, templateString)).toEqual(realUser);
30+
});
31+
});

0 commit comments

Comments
 (0)