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

Commit 68933c1

Browse files
authored
Merge pull request #5639 from matrix-org/dbkr/virtual_rooms_v2
VoIP virtual rooms, mk II
2 parents cc5ed3e + 648295e commit 68933c1

File tree

10 files changed

+231
-102
lines changed

10 files changed

+231
-102
lines changed

src/@types/global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import CountlyAnalytics from "../CountlyAnalytics";
3737
import UserActivity from "../UserActivity";
3838
import {ModalWidgetStore} from "../stores/ModalWidgetStore";
3939
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
40+
import VoipUserMapper from "../VoipUserMapper";
4041

4142
declare global {
4243
interface Window {
@@ -66,6 +67,7 @@ declare global {
6667
mxCountlyAnalytics: typeof CountlyAnalytics;
6768
mxUserActivity: UserActivity;
6869
mxModalWidgetStore: ModalWidgetStore;
70+
mxVoipUserMapper: VoipUserMapper;
6971
}
7072

7173
interface Document {

src/CallHandler.tsx

Lines changed: 91 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,19 @@ import { CallError } from "matrix-js-sdk/src/webrtc/call";
8383
import { logger } from 'matrix-js-sdk/src/logger';
8484
import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker"
8585
import { Action } from './dispatcher/actions';
86-
import { roomForVirtualRoom, getOrCreateVirtualRoomForRoom } from './VoipUserMapper';
86+
import VoipUserMapper from './VoipUserMapper';
8787
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
8888
import { randomString } from "matrix-js-sdk/src/randomstring";
8989

90-
const CHECK_PSTN_SUPPORT_ATTEMPTS = 3;
90+
export const PROTOCOL_PSTN = 'm.protocol.pstn';
91+
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
92+
export const PROTOCOL_SIP_NATIVE = 'im.vector.protocol.sip_native';
93+
export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual';
94+
95+
const CHECK_PROTOCOLS_ATTEMPTS = 3;
96+
// Event type for room account data and room creation content used to mark rooms as virtual rooms
97+
// (and store the ID of their native room)
98+
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
9199

92100
enum AudioID {
93101
Ring = 'ringAudio',
@@ -96,6 +104,29 @@ enum AudioID {
96104
Busy = 'busyAudio',
97105
}
98106

107+
interface ThirdpartyLookupResponseFields {
108+
/* eslint-disable camelcase */
109+
110+
// im.vector.sip_native
111+
virtual_mxid?: string;
112+
is_virtual?: boolean;
113+
114+
// im.vector.sip_virtual
115+
native_mxid?: string;
116+
is_native?: boolean;
117+
118+
// common
119+
lookup_success?: boolean;
120+
121+
/* eslint-enable camelcase */
122+
}
123+
124+
interface ThirdpartyLookupResponse {
125+
userid: string,
126+
protocol: string,
127+
fields: ThirdpartyLookupResponseFields,
128+
}
129+
99130
// Unlike 'CallType' in js-sdk, this one includes screen sharing
100131
// (because a screen sharing call is only a screen sharing call to the caller,
101132
// to the callee it's just a video call, at least as far as the current impl
@@ -126,7 +157,12 @@ export default class CallHandler {
126157
private audioPromises = new Map<AudioID, Promise<void>>();
127158
private dispatcherRef: string = null;
128159
private supportsPstnProtocol = null;
160+
private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol
161+
private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
129162
private pstnSupportCheckTimer: NodeJS.Timeout; // number actually because we're in the browser
163+
// For rooms we've been invited to, true if they're from virtual user, false if we've checked and they aren't.
164+
private invitedRoomsAreVirtual = new Map<string, boolean>();
165+
private invitedRoomCheckInProgress = false;
130166

131167
static sharedInstance() {
132168
if (!window.mxCallHandler) {
@@ -140,9 +176,9 @@ export default class CallHandler {
140176
* Gets the user-facing room associated with a call (call.roomId may be the call "virtual room"
141177
* if a voip_mxid_translate_pattern is set in the config)
142178
*/
143-
public static roomIdForCall(call: MatrixCall) {
179+
public static roomIdForCall(call: MatrixCall): string {
144180
if (!call) return null;
145-
return roomForVirtualRoom(call.roomId) || call.roomId;
181+
return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId;
146182
}
147183

148184
start() {
@@ -163,7 +199,7 @@ export default class CallHandler {
163199
MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming);
164200
}
165201

166-
this.checkForPstnSupport(CHECK_PSTN_SUPPORT_ATTEMPTS);
202+
this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS);
167203
}
168204

169205
stop() {
@@ -177,33 +213,73 @@ export default class CallHandler {
177213
}
178214
}
179215

180-
private async checkForPstnSupport(maxTries) {
216+
private async checkProtocols(maxTries) {
181217
try {
182218
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
183-
if (protocols['im.vector.protocol.pstn'] !== undefined) {
184-
this.supportsPstnProtocol = protocols['im.vector.protocol.pstn'];
185-
} else if (protocols['m.protocol.pstn'] !== undefined) {
186-
this.supportsPstnProtocol = protocols['m.protocol.pstn'];
219+
220+
if (protocols[PROTOCOL_PSTN] !== undefined) {
221+
this.supportsPstnProtocol = Boolean(protocols[PROTOCOL_PSTN]);
222+
if (this.supportsPstnProtocol) this.pstnSupportPrefixed = false;
223+
} else if (protocols[PROTOCOL_PSTN_PREFIXED] !== undefined) {
224+
this.supportsPstnProtocol = Boolean(protocols[PROTOCOL_PSTN_PREFIXED]);
225+
if (this.supportsPstnProtocol) this.pstnSupportPrefixed = true;
187226
} else {
188227
this.supportsPstnProtocol = null;
189228
}
229+
190230
dis.dispatch({action: Action.PstnSupportUpdated});
231+
232+
if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) {
233+
this.supportsSipNativeVirtual = Boolean(
234+
protocols[PROTOCOL_SIP_NATIVE] && protocols[PROTOCOL_SIP_VIRTUAL],
235+
);
236+
}
237+
238+
dis.dispatch({action: Action.VirtualRoomSupportUpdated});
191239
} catch (e) {
192240
if (maxTries === 1) {
193-
console.log("Failed to check for pstn protocol support and no retries remain: assuming no support", e);
241+
console.log("Failed to check for protocol support and no retries remain: assuming no support", e);
194242
} else {
195-
console.log("Failed to check for pstn protocol support: will retry", e);
243+
console.log("Failed to check for protocol support: will retry", e);
196244
this.pstnSupportCheckTimer = setTimeout(() => {
197-
this.checkForPstnSupport(maxTries - 1);
245+
this.checkProtocols(maxTries - 1);
198246
}, 10000);
199247
}
200248
}
201249
}
202250

203-
getSupportsPstnProtocol() {
251+
public getSupportsPstnProtocol() {
204252
return this.supportsPstnProtocol;
205253
}
206254

255+
public getSupportsVirtualRooms() {
256+
return this.supportsPstnProtocol;
257+
}
258+
259+
public pstnLookup(phoneNumber: string): Promise<ThirdpartyLookupResponse[]> {
260+
return MatrixClientPeg.get().getThirdpartyUser(
261+
this.pstnSupportPrefixed ? PROTOCOL_PSTN_PREFIXED : PROTOCOL_PSTN, {
262+
'm.id.phone': phoneNumber,
263+
},
264+
);
265+
}
266+
267+
public sipVirtualLookup(nativeMxid: string): Promise<ThirdpartyLookupResponse[]> {
268+
return MatrixClientPeg.get().getThirdpartyUser(
269+
PROTOCOL_SIP_VIRTUAL, {
270+
'native_mxid': nativeMxid,
271+
},
272+
);
273+
}
274+
275+
public sipNativeLookup(virtualMxid: string): Promise<ThirdpartyLookupResponse[]> {
276+
return MatrixClientPeg.get().getThirdpartyUser(
277+
PROTOCOL_SIP_NATIVE, {
278+
'virtual_mxid': virtualMxid,
279+
},
280+
);
281+
}
282+
207283
private onCallIncoming = (call) => {
208284
// we dispatch this synchronously to make sure that the event
209285
// handlers on the call are set up immediately (so that if
@@ -550,7 +626,7 @@ export default class CallHandler {
550626
Analytics.trackEvent('voip', 'placeCall', 'type', type);
551627
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
552628

553-
const mappedRoomId = (await getOrCreateVirtualRoomForRoom(roomId)) || roomId;
629+
const mappedRoomId = (await VoipUserMapper.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId;
554630
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
555631

556632
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);

src/SlashCommands.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,9 +1040,7 @@ export const Commands = [
10401040

10411041
return success((async () => {
10421042
if (isPhoneNumber) {
1043-
const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', {
1044-
'm.id.phone': userId,
1045-
});
1043+
const results = await CallHandler.sharedInstance().pstnLookup(this.state.value);
10461044
if (!results || results.length === 0 || !results[0].userid) {
10471045
throw new Error("Unable to find Matrix ID for phone number");
10481046
}

src/VoipUserMapper.ts

Lines changed: 79 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,66 +14,97 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { ensureDMExists, findDMForUser } from './createRoom';
17+
import { ensureVirtualRoomExists, findDMForUser } from './createRoom';
1818
import { MatrixClientPeg } from "./MatrixClientPeg";
1919
import DMRoomMap from "./utils/DMRoomMap";
20-
import SdkConfig from "./SdkConfig";
20+
import CallHandler, { VIRTUAL_ROOM_EVENT_TYPE } from './CallHandler';
21+
import { Room } from 'matrix-js-sdk/src/models/room';
2122

22-
// Functions for mapping users & rooms for the voip_mxid_translate_pattern
23-
// config option
23+
// Functions for mapping virtual users & rooms. Currently the only lookup
24+
// is sip virtual: there could be others in the future.
2425

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-
}
26+
export default class VoipUserMapper {
27+
private virtualRoomIdCache = new Set<string>();
3528

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;
29+
public static sharedInstance(): VoipUserMapper {
30+
if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper();
31+
return window.mxVoipUserMapper;
32+
}
4033

41-
const regexString = templateString.replace('${mxid}', '(.+)');
34+
private async userToVirtualUser(userId: string): Promise<string> {
35+
const results = await CallHandler.sharedInstance().sipVirtualLookup(userId);
36+
if (results.length === 0) return null;
37+
return results[0].userid;
38+
}
4239

43-
const match = userId.match('^' + regexString + '$');
44-
if (!match) return null;
40+
public async getOrCreateVirtualRoomForRoom(roomId: string):Promise<string> {
41+
const userId = DMRoomMap.shared().getUserIdForRoomId(roomId);
42+
if (!userId) return null;
4543

46-
return decodeURIComponent(match[1].replace(/=/g, '%'));
47-
}
44+
const virtualUser = await this.userToVirtualUser(userId);
45+
if (!virtualUser) return null;
4846

49-
async function getOrCreateVirtualRoomForUser(userId: string):Promise<string> {
50-
const virtualUser = userToVirtualUser(userId);
51-
if (!virtualUser) return null;
47+
const virtualRoomId = await ensureVirtualRoomExists(MatrixClientPeg.get(), virtualUser, roomId);
48+
MatrixClientPeg.get().setRoomAccountData(virtualRoomId, VIRTUAL_ROOM_EVENT_TYPE, {
49+
native_room: roomId,
50+
});
5251

53-
return await ensureDMExists(MatrixClientPeg.get(), virtualUser);
54-
}
52+
return virtualRoomId;
53+
}
5554

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-
}
55+
public nativeRoomForVirtualRoom(roomId: string):string {
56+
const virtualRoom = MatrixClientPeg.get().getRoom(roomId);
57+
if (!virtualRoom) return null;
58+
const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
59+
if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null;
60+
return virtualRoomEvent.getContent()['native_room'] || null;
61+
}
6162

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;
63+
public isVirtualRoom(room: Room):boolean {
64+
if (this.nativeRoomForVirtualRoom(room.roomId)) return true;
65+
66+
if (this.virtualRoomIdCache.has(room.roomId)) return true;
67+
68+
// also look in the create event for the claimed native room ID, which is the only
69+
// way we can recognise a virtual room we've created when it first arrives down
70+
// our stream. We don't trust this in general though, as it could be faked by an
71+
// inviter: our main source of truth is the DM state.
72+
const roomCreateEvent = room.currentState.getStateEvents("m.room.create", "");
73+
if (!roomCreateEvent || !roomCreateEvent.getContent()) return false;
74+
// we only look at this for rooms we created (so inviters can't just cause rooms
75+
// to be invisible)
76+
if (roomCreateEvent.getSender() !== MatrixClientPeg.get().getUserId()) return false;
77+
const claimedNativeRoomId = roomCreateEvent.getContent()[VIRTUAL_ROOM_EVENT_TYPE];
78+
return Boolean(claimedNativeRoomId);
7179
}
72-
}
7380

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);
81+
public async onNewInvitedRoom(invitedRoom: Room) {
82+
const inviterId = invitedRoom.getDMInviter();
83+
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
84+
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
85+
if (result.length === 0) {
86+
return true;
87+
}
88+
89+
if (result[0].fields.is_virtual) {
90+
const nativeUser = result[0].userid;
91+
const nativeRoom = findDMForUser(MatrixClientPeg.get(), nativeUser);
92+
if (nativeRoom) {
93+
// It's a virtual room with a matching native room, so set the room account data. This
94+
// will make sure we know where how to map calls and also allow us know not to display
95+
// it in the future.
96+
MatrixClientPeg.get().setRoomAccountData(invitedRoom.roomId, VIRTUAL_ROOM_EVENT_TYPE, {
97+
native_room: nativeRoom.roomId,
98+
});
99+
// also auto-join the virtual room if we have a matching native room
100+
// (possibly we should only join if we've also joined the native room, then we'd also have
101+
// to make sure we joined virtual rooms on joining a native one)
102+
MatrixClientPeg.get().joinRoom(invitedRoom.roomId);
103+
}
104+
105+
// also put this room in the virtual room ID cache so isVirtualRoom return the right answer
106+
// in however long it takes for the echo of setAccountData to come down the sync
107+
this.virtualRoomIdCache.add(invitedRoom.roomId);
108+
}
109+
}
79110
}

src/components/views/voip/DialPadModal.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import DialPad from './DialPad';
2424
import dis from '../../../dispatcher/dispatcher';
2525
import Modal from "../../../Modal";
2626
import ErrorDialog from "../../views/dialogs/ErrorDialog";
27+
import CallHandler from "../../../CallHandler";
2728

2829
interface IProps {
2930
onFinished: (boolean) => void;
@@ -64,9 +65,7 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
6465
}
6566

6667
onDialPress = async () => {
67-
const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', {
68-
'm.id.phone': this.state.value,
69-
});
68+
const results = await CallHandler.sharedInstance().pstnLookup(this.state.value);
7069
if (!results || results.length === 0 || !results[0].userid) {
7170
Modal.createTrackedDialog('', '', ErrorDialog, {
7271
title: _t("Unable to look up phone number"),

0 commit comments

Comments
 (0)