From 196507a730a6e64665fbac54f2d1c682c311783d Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 12 Feb 2021 20:55:54 +0000 Subject: [PATCH 01/10] VoIP virtual rooms, mk II Does a thirdparty protocol lookup to the homeserver to get the corresponding native/virtual user for a matrix ID. Stores the mappings in room account data. Involves some slightly nasty workarounds for that fact that room account data has no local echo. --- src/@types/global.d.ts | 2 + src/CallHandler.tsx | 88 +++++++++--- src/SlashCommands.tsx | 4 +- src/VoipUserMapper.ts | 127 +++++++++++------- src/components/views/voip/DialPadModal.tsx | 5 +- src/dispatcher/actions.ts | 7 + src/stores/room-list/RoomListStore.ts | 37 +++++ .../room-list/filters/VisibilityProvider.ts | 8 +- test/VoipUserMapper-test.ts | 31 ----- 9 files changed, 208 insertions(+), 101 deletions(-) delete mode 100644 test/VoipUserMapper-test.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 2a28c8e43f..28f22780a2 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -37,6 +37,7 @@ import CountlyAnalytics from "../CountlyAnalytics"; import UserActivity from "../UserActivity"; import {ModalWidgetStore} from "../stores/ModalWidgetStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; +import VoipUserMapper from "../VoipUserMapper"; declare global { interface Window { @@ -66,6 +67,7 @@ declare global { mxCountlyAnalytics: typeof CountlyAnalytics; mxUserActivity: UserActivity; mxModalWidgetStore: ModalWidgetStore; + mxVoipUserMapper: VoipUserMapper; } interface Document { diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index a6d3534fa1..ca9f96c7c2 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -84,10 +84,17 @@ import { CallError } from "matrix-js-sdk/src/webrtc/call"; import { logger } from 'matrix-js-sdk/src/logger'; import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker" import { Action } from './dispatcher/actions'; -import { roomForVirtualRoom, getOrCreateVirtualRoomForRoom } from './VoipUserMapper'; +import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; -const CHECK_PSTN_SUPPORT_ATTEMPTS = 3; +export const PROTOCOL_PSTN = 'm.protocol.pstn'; +export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; +export const PROTOCOL_SIP_NATIVE = 'im.vector.protocol.sip_native'; +export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual'; + +const CHECK_PROTOCOLS_ATTEMPTS = 3; +// Event type for room account data used to mark rooms as virtual rooms (and store the ID of their native room) +export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room'; enum AudioID { Ring = 'ringAudio', @@ -96,6 +103,12 @@ enum AudioID { Busy = 'busyAudio', } +interface ThirpartyLookupResponse { + userid: string, + protocol: string, + fields: {[key: string]: any}, +} + // Unlike 'CallType' in js-sdk, this one includes screen sharing // (because a screen sharing call is only a screen sharing call to the caller, // to the callee it's just a video call, at least as far as the current impl @@ -126,7 +139,12 @@ export default class CallHandler { private audioPromises = new Map>(); private dispatcherRef: string = null; private supportsPstnProtocol = null; + private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol + private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native private pstnSupportCheckTimer: NodeJS.Timeout; // number actually because we're in the browser + // For rooms we've been invited to, true if they're from virtual user, false if we've checked and they aren't. + private invitedRoomsAreVirtual = new Map(); + private invitedRoomCheckInProgress = false; static sharedInstance() { if (!window.mxCallHandler) { @@ -140,9 +158,9 @@ export default class CallHandler { * Gets the user-facing room associated with a call (call.roomId may be the call "virtual room" * if a voip_mxid_translate_pattern is set in the config) */ - public static roomIdForCall(call: MatrixCall) { + public static roomIdForCall(call: MatrixCall): string { if (!call) return null; - return roomForVirtualRoom(call.roomId) || call.roomId; + return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId; } start() { @@ -163,7 +181,7 @@ export default class CallHandler { MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming); } - this.checkForPstnSupport(CHECK_PSTN_SUPPORT_ATTEMPTS); + this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS); } stop() { @@ -177,33 +195,73 @@ export default class CallHandler { } } - private async checkForPstnSupport(maxTries) { + private async checkProtocols(maxTries) { try { const protocols = await MatrixClientPeg.get().getThirdpartyProtocols(); - if (protocols['im.vector.protocol.pstn'] !== undefined) { - this.supportsPstnProtocol = protocols['im.vector.protocol.pstn']; - } else if (protocols['m.protocol.pstn'] !== undefined) { - this.supportsPstnProtocol = protocols['m.protocol.pstn']; + + if (protocols[PROTOCOL_PSTN] !== undefined) { + this.supportsPstnProtocol = Boolean(protocols[PROTOCOL_PSTN]); + if (this.supportsPstnProtocol) this.pstnSupportPrefixed = false; + } else if (protocols[PROTOCOL_PSTN_PREFIXED] !== undefined) { + this.supportsPstnProtocol = Boolean(protocols[PROTOCOL_PSTN_PREFIXED]); + if (this.supportsPstnProtocol) this.pstnSupportPrefixed = true; } else { this.supportsPstnProtocol = null; } + dis.dispatch({action: Action.PstnSupportUpdated}); + + if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) { + this.supportsSipNativeVirtual = Boolean( + protocols[PROTOCOL_SIP_NATIVE] && protocols[PROTOCOL_SIP_VIRTUAL], + ); + } + + dis.dispatch({action: Action.VirtualRoomSupportUpdated}); } catch (e) { if (maxTries === 1) { - console.log("Failed to check for pstn protocol support and no retries remain: assuming no support", e); + console.log("Failed to check for protocol support and no retries remain: assuming no support", e); } else { - console.log("Failed to check for pstn protocol support: will retry", e); + console.log("Failed to check for protocol support: will retry", e); this.pstnSupportCheckTimer = setTimeout(() => { - this.checkForPstnSupport(maxTries - 1); + this.checkProtocols(maxTries - 1); }, 10000); } } } - getSupportsPstnProtocol() { + public getSupportsPstnProtocol() { return this.supportsPstnProtocol; } + public getSupportsVirtualRooms() { + return this.supportsPstnProtocol; + } + + public pstnLookup(phoneNumber: string): Promise { + return MatrixClientPeg.get().getThirdpartyUser( + this.pstnSupportPrefixed ? PROTOCOL_PSTN_PREFIXED : PROTOCOL_PSTN, { + 'm.id.phone': phoneNumber, + }, + ); + } + + public sipVirtualLookup(nativeMxid: string): Promise { + return MatrixClientPeg.get().getThirdpartyUser( + PROTOCOL_SIP_VIRTUAL, { + 'native_mxid': nativeMxid, + }, + ); + } + + public sipNativeLookup(virtualMxid: string): Promise { + return MatrixClientPeg.get().getThirdpartyUser( + PROTOCOL_SIP_NATIVE, { + 'virtual_mxid': virtualMxid, + }, + ); + } + private onCallIncoming = (call) => { // we dispatch this synchronously to make sure that the event // handlers on the call are set up immediately (so that if @@ -543,7 +601,7 @@ export default class CallHandler { Analytics.trackEvent('voip', 'placeCall', 'type', type); CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false); - const mappedRoomId = (await getOrCreateVirtualRoomForRoom(roomId)) || roomId; + const mappedRoomId = (await VoipUserMapper.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId; logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId); const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId); diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index a39ad33b04..8f20754027 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1040,9 +1040,7 @@ export const Commands = [ return success((async () => { if (isPhoneNumber) { - const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', { - 'm.id.phone': userId, - }); + const results = await CallHandler.sharedInstance().pstnLookup(this.state.value); if (!results || results.length === 0 || !results[0].userid) { throw new Error("Unable to find Matrix ID for phone number"); } diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index a4f5822065..c317402327 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -17,63 +17,96 @@ limitations under the License. import { ensureDMExists, findDMForUser } from './createRoom'; import { MatrixClientPeg } from "./MatrixClientPeg"; import DMRoomMap from "./utils/DMRoomMap"; -import SdkConfig from "./SdkConfig"; +import CallHandler, { VIRTUAL_ROOM_EVENT_TYPE } from './CallHandler'; +import RoomListStore from './stores/room-list/RoomListStore'; +import { Room } from 'matrix-js-sdk/src/models/room'; -// Functions for mapping users & rooms for the voip_mxid_translate_pattern -// config option +// Functions for mapping virtual users & rooms. Currently the only lookup +// is sip virtual: there could be others in the future. -export function voipUserMapperEnabled(): boolean { - return SdkConfig.get()['voip_mxid_translate_pattern'] !== undefined; -} +export default class VoipUserMapper { + private virtualRoomIdCache = new Set(); -// only exported for tests -export function userToVirtualUser(userId: string, templateString?: string): string { - if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern']; - if (!templateString) return null; - return templateString.replace('${mxid}', encodeURIComponent(userId).replace(/%/g, '=').toLowerCase()); -} + public static sharedInstance(): VoipUserMapper { + if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper(); + return window.mxVoipUserMapper; + } -// only exported for tests -export function virtualUserToUser(userId: string, templateString?: string): string { - if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern']; - if (!templateString) return null; + private async userToVirtualUser(userId: string): Promise { + const results = await CallHandler.sharedInstance().sipVirtualLookup(userId); + if (results.length === 0) return null; + return results[0].userid; + } - const regexString = templateString.replace('${mxid}', '(.+)'); + public async getOrCreateVirtualRoomForRoom(roomId: string):Promise { + const userId = DMRoomMap.shared().getUserIdForRoomId(roomId); + if (!userId) return null; - const match = userId.match('^' + regexString + '$'); - if (!match) return null; + const virtualUser = await this.userToVirtualUser(userId); + if (!virtualUser) return null; - return decodeURIComponent(match[1].replace(/=/g, '%')); -} + // There's quite a bit of acromatics here to prevent the virtual room being shown + // while it's being created: forstly, we have to stop the RoomListStore from showing + // new rooms for a bit, because we can't set the room account data to say it's a virtual + // room until we have the room ID. Secondly, once we have the new room ID, we have to + // temporarily cache the fact it's a virtual room because there's no local echo on + // room account data so it won't show up in the room model until it comes down the + // sync stream again. Ick. + RoomListStore.instance.startHoldingNewRooms(); + try { + const virtualRoomId = await ensureDMExists(MatrixClientPeg.get(), virtualUser); + MatrixClientPeg.get().setRoomAccountData(virtualRoomId, VIRTUAL_ROOM_EVENT_TYPE, { + native_room: roomId, + }); + this.virtualRoomIdCache.add(virtualRoomId); -async function getOrCreateVirtualRoomForUser(userId: string):Promise { - const virtualUser = userToVirtualUser(userId); - if (!virtualUser) return null; + return virtualRoomId; + } finally { + RoomListStore.instance.stopHoldingNewRooms(); + } + } - return await ensureDMExists(MatrixClientPeg.get(), virtualUser); -} + public nativeRoomForVirtualRoom(roomId: string):string { + const virtualRoom = MatrixClientPeg.get().getRoom(roomId); + if (!virtualRoom) return null; + const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); + if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null; + return virtualRoomEvent.getContent()['native_room'] || null; + } -export async function getOrCreateVirtualRoomForRoom(roomId: string):Promise { - const user = DMRoomMap.shared().getUserIdForRoomId(roomId); - if (!user) return null; - return getOrCreateVirtualRoomForUser(user); -} + public isVirtualRoom(roomId: string):boolean { + if (this.nativeRoomForVirtualRoom(roomId)) return true; -export function roomForVirtualRoom(roomId: string):string { - const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId); - if (!virtualUser) return null; - const realUser = virtualUserToUser(virtualUser); - const room = findDMForUser(MatrixClientPeg.get(), realUser); - if (room) { - return room.roomId; - } else { - return null; + return this.virtualRoomIdCache.has(roomId); + } + + public async onNewInvitedRoom(invitedRoom: Room) { + const inviterId = invitedRoom.getDMInviter(); + console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); + const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); + if (result.length === 0) { + return true; + } + + if (result[0].fields.is_virtual) { + const nativeUser = result[0].userid; + const nativeRoom = findDMForUser(MatrixClientPeg.get(), nativeUser); + if (nativeRoom) { + // It's a virtual room with a matching native room, so set the room account data. This + // will make sure we know where how to map calls and also allow us know not to display + // it in the future. + MatrixClientPeg.get().setRoomAccountData(invitedRoom.roomId, VIRTUAL_ROOM_EVENT_TYPE, { + native_room: nativeRoom.roomId, + }); + // also auto-join the virtual room if we have a matching native room + // (possibly we should only join if we've also joined the native room, then we'd also have + // to make sure we joined virtual rooms on joining a native one) + MatrixClientPeg.get().joinRoom(invitedRoom.roomId); + } + + // also put this room in the virtual room ID cache so isVirtualRoom return the right answer + // in however long it takes for the echo of setAccountData to come down the sync + this.virtualRoomIdCache.add(invitedRoom.roomId); + } } } - -export function isVirtualRoom(roomId: string):boolean { - const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId); - if (!virtualUser) return null; - const realUser = virtualUserToUser(virtualUser); - return Boolean(realUser); -} diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx index 74b39e0721..9f031a48a3 100644 --- a/src/components/views/voip/DialPadModal.tsx +++ b/src/components/views/voip/DialPadModal.tsx @@ -24,6 +24,7 @@ import DialPad from './DialPad'; import dis from '../../../dispatcher/dispatcher'; import Modal from "../../../Modal"; import ErrorDialog from "../../views/dialogs/ErrorDialog"; +import CallHandler from "../../../CallHandler"; interface IProps { onFinished: (boolean) => void; @@ -64,9 +65,7 @@ export default class DialpadModal extends React.PureComponent { } onDialPress = async () => { - const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', { - 'm.id.phone': this.state.value, - }); + const results = await CallHandler.sharedInstance().pstnLookup(this.state.value); if (!results || results.length === 0 || !results[0].userid) { Modal.createTrackedDialog('', '', ErrorDialog, { title: _t("Unable to look up phone number"), diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index ce27f9b289..12bf4c57a3 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -106,4 +106,11 @@ export enum Action { * XXX: Is an action the right thing for this? */ PstnSupportUpdated = "pstn_support_updated", + + /** + * Similar to PstnSupportUpdated, fired when CallHandler has checked for virtual room support + * payload: none + * XXX: Ditto + */ + VirtualRoomSupportUpdated = "virtual_room_support_updated", } diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 24a75c53e7..15e385f7e4 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -35,6 +35,7 @@ import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import { NameFilterCondition } from "./filters/NameFilterCondition"; import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore"; import { VisibilityProvider } from "./filters/VisibilityProvider"; +import VoipUserMapper from "../../VoipUserMapper"; interface IState { tagsEnabled?: boolean; @@ -63,6 +64,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } this.emit(LISTS_UPDATE_EVENT); }); + // When new rooms arrive, we may hold them here until we have enough info to know whether we should before display them. + private roomHoldingPen: Room[] = []; + private holdNewRooms = false; private readonly watchedSettings = [ 'feature_custom_tags', @@ -126,6 +130,24 @@ export class RoomListStoreClass extends AsyncStoreWithClient { this.updateFn.trigger(); } + // After calling this, any new rooms that appear are not displayed until stopHoldingNewRooms() + // is called. Be sure to always call this in a try/finally block to ensure stopHoldingNewRooms + // is called afterwards. + public startHoldingNewRooms() { + console.log("hold-new-rooms mode enabled."); + this.holdNewRooms = true; + } + + public stopHoldingNewRooms() { + console.log("hold-new-rooms mode disabled: processing " + this.roomHoldingPen.length + " held rooms"); + this.holdNewRooms = false; + for (const heldRoom of this.roomHoldingPen) { + console.log("Processing held room: " + heldRoom.roomId); + this.handleRoomUpdate(heldRoom, RoomUpdateCause.NewRoom); + } + this.roomHoldingPen = []; + } + private checkLoggingEnabled() { if (SettingsStore.getValue("advancedRoomListLogging")) { console.warn("Advanced room list logging is enabled"); @@ -398,6 +420,21 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { + if (cause === RoomUpdateCause.NewRoom) { + if (this.holdNewRooms) { + console.log("Room updates are held: putting room " + room.roomId + " into the holding pen"); + this.roomHoldingPen.push(room); + return; + } else { + // we call straight out to VoipUserMapper here which is a bit of a hack: probably this + // should be calling the visibility provider which in turn farms out to various visibility + // providers? Anyway, the point of this is that we delay doing anything about this room + // until the VoipUserMapper had had a chance to do the things it needs to do to decide + // if we should show this room or not. + await VoipUserMapper.sharedInstance().onNewInvitedRoom(room); + } + } + if (!VisibilityProvider.instance.isRoomVisible(room)) { return; // don't do anything on rooms that aren't visible } diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts index 6bc790bb1e..45233357c1 100644 --- a/src/stores/room-list/filters/VisibilityProvider.ts +++ b/src/stores/room-list/filters/VisibilityProvider.ts @@ -15,8 +15,9 @@ */ import {Room} from "matrix-js-sdk/src/models/room"; +import CallHandler from "../../../CallHandler"; import { RoomListCustomisations } from "../../../customisations/RoomList"; -import { isVirtualRoom, voipUserMapperEnabled } from "../../../VoipUserMapper"; +import VoipUserMapper from "../../../VoipUserMapper"; export class VisibilityProvider { private static internalInstance: VisibilityProvider; @@ -35,7 +36,10 @@ export class VisibilityProvider { let isVisible = true; // Returned at the end of this function let forced = false; // When true, this function won't bother calling the customisation points - if (voipUserMapperEnabled() && isVirtualRoom(room.roomId)) { + if ( + CallHandler.sharedInstance().getSupportsVirtualRooms() && + VoipUserMapper.sharedInstance().isVirtualRoom(room.roomId) + ) { isVisible = false; forced = true; } diff --git a/test/VoipUserMapper-test.ts b/test/VoipUserMapper-test.ts deleted file mode 100644 index ee45379e4c..0000000000 --- a/test/VoipUserMapper-test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { userToVirtualUser, virtualUserToUser } from '../src/VoipUserMapper'; - -const templateString = '@_greatappservice_${mxid}:frooble.example'; -const realUser = '@alice:boop.example'; -const virtualUser = "@_greatappservice_=40alice=3aboop.example:frooble.example"; - -describe('VoipUserMapper', function() { - it('translates users to virtual users', function() { - expect(userToVirtualUser(realUser, templateString)).toEqual(virtualUser); - }); - - it('translates users to virtual users', function() { - expect(virtualUserToUser(virtualUser, templateString)).toEqual(realUser); - }); -}); From d7bf57af13de7aeb03bed98df0649071ca6a0b17 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 15 Feb 2021 15:04:01 +0000 Subject: [PATCH 02/10] Add missing 'd' --- src/CallHandler.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index ca9f96c7c2..6cebae1093 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -103,7 +103,7 @@ enum AudioID { Busy = 'busyAudio', } -interface ThirpartyLookupResponse { +interface ThirdpartyLookupResponse { userid: string, protocol: string, fields: {[key: string]: any}, @@ -238,7 +238,7 @@ export default class CallHandler { return this.supportsPstnProtocol; } - public pstnLookup(phoneNumber: string): Promise { + public pstnLookup(phoneNumber: string): Promise { return MatrixClientPeg.get().getThirdpartyUser( this.pstnSupportPrefixed ? PROTOCOL_PSTN_PREFIXED : PROTOCOL_PSTN, { 'm.id.phone': phoneNumber, @@ -246,7 +246,7 @@ export default class CallHandler { ); } - public sipVirtualLookup(nativeMxid: string): Promise { + public sipVirtualLookup(nativeMxid: string): Promise { return MatrixClientPeg.get().getThirdpartyUser( PROTOCOL_SIP_VIRTUAL, { 'native_mxid': nativeMxid, @@ -254,7 +254,7 @@ export default class CallHandler { ); } - public sipNativeLookup(virtualMxid: string): Promise { + public sipNativeLookup(virtualMxid: string): Promise { return MatrixClientPeg.get().getThirdpartyUser( PROTOCOL_SIP_NATIVE, { 'virtual_mxid': virtualMxid, From 554dfdb6bf58a82f9f09b047f2cca6835a733d57 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 15 Feb 2021 15:05:01 +0000 Subject: [PATCH 03/10] Typo Co-authored-by: J. Ryan Stinnett --- src/VoipUserMapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index c317402327..bf592de001 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -45,7 +45,7 @@ export default class VoipUserMapper { const virtualUser = await this.userToVirtualUser(userId); if (!virtualUser) return null; - // There's quite a bit of acromatics here to prevent the virtual room being shown + // There's quite a bit of acrobatics here to prevent the virtual room being shown // while it's being created: forstly, we have to stop the RoomListStore from showing // new rooms for a bit, because we can't set the room account data to say it's a virtual // room until we have the room ID. Secondly, once we have the new room ID, we have to From 0b574327d77e267422eb9929358315e8810c52f9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 15 Feb 2021 15:05:16 +0000 Subject: [PATCH 04/10] More typo Co-authored-by: J. Ryan Stinnett --- src/VoipUserMapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index bf592de001..ceb87c1829 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -46,7 +46,7 @@ export default class VoipUserMapper { if (!virtualUser) return null; // There's quite a bit of acrobatics here to prevent the virtual room being shown - // while it's being created: forstly, we have to stop the RoomListStore from showing + // while it's being created: firstly, we have to stop the RoomListStore from showing // new rooms for a bit, because we can't set the room account data to say it's a virtual // room until we have the room ID. Secondly, once we have the new room ID, we have to // temporarily cache the fact it's a virtual room because there's no local echo on From d339dc447f83ab82c9f135e355eb140e237f0678 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 15 Feb 2021 15:25:07 +0000 Subject: [PATCH 05/10] Add specific fields to third party lookup response fields --- src/CallHandler.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 6cebae1093..63b849af4b 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -103,10 +103,27 @@ enum AudioID { Busy = 'busyAudio', } +interface ThirdpartyLookupResponseFields { + /* eslint-disable camelcase */ + + // im.vector.sip_native + virtual_mxid: string; + is_virtual: boolean; + + // im.vector.sip_virtual + native_mxid: string; + is_native: boolean; + + // common + lookup_success: boolean; + + /* eslint-enable camelcase */ +} + interface ThirdpartyLookupResponse { userid: string, protocol: string, - fields: {[key: string]: any}, + fields: ThirdpartyLookupResponseFields, } // Unlike 'CallType' in js-sdk, this one includes screen sharing From 89b2dae035f33728c45a8193d234d688b6ba209b Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 15 Feb 2021 18:13:13 +0000 Subject: [PATCH 06/10] Send onNewInvitedRoom via VisibilityProvider --- src/stores/room-list/RoomListStore.ts | 12 ++++++------ src/stores/room-list/filters/VisibilityProvider.ts | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 15e385f7e4..a050442eae 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -426,12 +426,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient { this.roomHoldingPen.push(room); return; } else { - // we call straight out to VoipUserMapper here which is a bit of a hack: probably this - // should be calling the visibility provider which in turn farms out to various visibility - // providers? Anyway, the point of this is that we delay doing anything about this room - // until the VoipUserMapper had had a chance to do the things it needs to do to decide - // if we should show this room or not. - await VoipUserMapper.sharedInstance().onNewInvitedRoom(room); + // Let the visibility provider know that there is a new invited room. It would be nice + // if this could just be an event that things listen for but the point of this is that + // we delay doing anything about this room until the VoipUserMapper had had a chance + // to do the things it needs to do to decide if we should show this room or not, so + // an even wouldn't et us do that. + await VisibilityProvider.instance.onNewInvitedRoom(room); } } diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts index 45233357c1..2239f9e1ac 100644 --- a/src/stores/room-list/filters/VisibilityProvider.ts +++ b/src/stores/room-list/filters/VisibilityProvider.ts @@ -32,6 +32,10 @@ export class VisibilityProvider { return VisibilityProvider.internalInstance; } + public async onNewInvitedRoom(room: Room) { + await VoipUserMapper.sharedInstance().onNewInvitedRoom(room); + } + public isRoomVisible(room: Room): boolean { let isVisible = true; // Returned at the end of this function let forced = false; // When true, this function won't bother calling the customisation points From 79455d99b477d42f1738120780a34f83e903b1a2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 15 Feb 2021 19:38:17 +0000 Subject: [PATCH 07/10] Unused import --- src/stores/room-list/RoomListStore.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index a050442eae..f420dd2b08 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -35,7 +35,6 @@ import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import { NameFilterCondition } from "./filters/NameFilterCondition"; import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore"; import { VisibilityProvider } from "./filters/VisibilityProvider"; -import VoipUserMapper from "../../VoipUserMapper"; interface IState { tagsEnabled?: boolean; From 3b16645b59b7df1ee6881f91a62df34761b41c90 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 16 Feb 2021 18:52:49 +0000 Subject: [PATCH 08/10] Make fields optional --- src/CallHandler.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 63b849af4b..b42b2cdc7c 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -107,15 +107,15 @@ interface ThirdpartyLookupResponseFields { /* eslint-disable camelcase */ // im.vector.sip_native - virtual_mxid: string; - is_virtual: boolean; + virtual_mxid?: string; + is_virtual?: boolean; // im.vector.sip_virtual - native_mxid: string; - is_native: boolean; + native_mxid?: string; + is_native?: boolean; // common - lookup_success: boolean; + lookup_success?: boolean; /* eslint-enable camelcase */ } From 6130bdf0d2b944028ba032124c72887f67eb419e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 17 Feb 2021 18:51:21 +0000 Subject: [PATCH 09/10] Use creation content to signal virtual-ness This makes things a lot simpler. --- src/CallHandler.tsx | 3 +- src/VoipUserMapper.ts | 44 ++++++++----------- src/createRoom.ts | 30 +++++++++++++ src/stores/room-list/RoomListStore.ts | 39 +++------------- .../room-list/filters/VisibilityProvider.ts | 2 +- 5 files changed, 58 insertions(+), 60 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index af13c66e8a..f73424fd4d 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -93,7 +93,8 @@ export const PROTOCOL_SIP_NATIVE = 'im.vector.protocol.sip_native'; export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual'; const CHECK_PROTOCOLS_ATTEMPTS = 3; -// Event type for room account data used to mark rooms as virtual rooms (and store the ID of their native room) +// Event type for room account data and room creation content used to mark rooms as virtual rooms +// (and store the ID of their native room) export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room'; enum AudioID { diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index ceb87c1829..5f4e33dc04 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -14,19 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ensureDMExists, findDMForUser } from './createRoom'; +import { ensureVirtualRoomExists, findDMForUser } from './createRoom'; import { MatrixClientPeg } from "./MatrixClientPeg"; import DMRoomMap from "./utils/DMRoomMap"; import CallHandler, { VIRTUAL_ROOM_EVENT_TYPE } from './CallHandler'; -import RoomListStore from './stores/room-list/RoomListStore'; import { Room } from 'matrix-js-sdk/src/models/room'; // Functions for mapping virtual users & rooms. Currently the only lookup // is sip virtual: there could be others in the future. export default class VoipUserMapper { - private virtualRoomIdCache = new Set(); - public static sharedInstance(): VoipUserMapper { if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper(); return window.mxVoipUserMapper; @@ -45,25 +42,12 @@ export default class VoipUserMapper { const virtualUser = await this.userToVirtualUser(userId); if (!virtualUser) return null; - // There's quite a bit of acrobatics here to prevent the virtual room being shown - // while it's being created: firstly, we have to stop the RoomListStore from showing - // new rooms for a bit, because we can't set the room account data to say it's a virtual - // room until we have the room ID. Secondly, once we have the new room ID, we have to - // temporarily cache the fact it's a virtual room because there's no local echo on - // room account data so it won't show up in the room model until it comes down the - // sync stream again. Ick. - RoomListStore.instance.startHoldingNewRooms(); - try { - const virtualRoomId = await ensureDMExists(MatrixClientPeg.get(), virtualUser); - MatrixClientPeg.get().setRoomAccountData(virtualRoomId, VIRTUAL_ROOM_EVENT_TYPE, { - native_room: roomId, - }); - this.virtualRoomIdCache.add(virtualRoomId); + const virtualRoomId = await ensureVirtualRoomExists(MatrixClientPeg.get(), virtualUser, roomId); + MatrixClientPeg.get().setRoomAccountData(virtualRoomId, VIRTUAL_ROOM_EVENT_TYPE, { + native_room: roomId, + }); - return virtualRoomId; - } finally { - RoomListStore.instance.stopHoldingNewRooms(); - } + return virtualRoomId; } public nativeRoomForVirtualRoom(roomId: string):string { @@ -74,10 +58,20 @@ export default class VoipUserMapper { return virtualRoomEvent.getContent()['native_room'] || null; } - public isVirtualRoom(roomId: string):boolean { - if (this.nativeRoomForVirtualRoom(roomId)) return true; + public isVirtualRoom(room: Room):boolean { + if (this.nativeRoomForVirtualRoom(room.roomId)) return true; - return this.virtualRoomIdCache.has(roomId); + // also look in the create event for the claimed native room ID, which is the only + // way we can recognise a virtual room we've created when it first arrives down + // our stream. We don't trust this in general though, as it could be faked by an + // inviter: our main source of truth is the DM state. + const roomCreateEvent = room.currentState.getStateEvents("m.room.create", ""); + if (!roomCreateEvent || !roomCreateEvent.getContent()) return false; + // we only look at this for rooms we created (so inviters can't just cause rooms + // to be invisible) + if (roomCreateEvent.getSender() !== MatrixClientPeg.get().getUserId()) return false; + const claimedNativeRoomId = roomCreateEvent.getContent()[VIRTUAL_ROOM_EVENT_TYPE]; + return Boolean(claimedNativeRoomId); } public async onNewInvitedRoom(invitedRoom: Room) { diff --git a/src/createRoom.ts b/src/createRoom.ts index 699df0d799..9e3960cdb7 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -30,6 +30,7 @@ import { getE2EEWellKnown } from "./utils/WellKnownUtils"; import GroupStore from "./stores/GroupStore"; import CountlyAnalytics from "./CountlyAnalytics"; import { isJoinedOrNearlyJoined } from "./utils/membership"; +import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler"; // we define a number of interfaces which take their names from the js-sdk /* eslint-disable camelcase */ @@ -300,6 +301,34 @@ export async function canEncryptToAllUsers(client: MatrixClient, userIds: string } } +// Similar to ensureDMExists but also adds creation content +// without polluting ensureDMExists with unrelated stuff (also +// they're never encrypted). +export async function ensureVirtualRoomExists( + client: MatrixClient, userId: string, nativeRoomId: string, +): Promise { + const existingDMRoom = findDMForUser(client, userId); + let roomId; + if (existingDMRoom) { + roomId = existingDMRoom.roomId; + } else { + roomId = await createRoom({ + dmUserId: userId, + spinner: false, + andView: false, + createOpts: { + creation_content: { + // This allows us to recognise that the room is a virtual room + // when it comes down our sync stream (we also put the ID of the + // respective native room in there because why not?) + [VIRTUAL_ROOM_EVENT_TYPE]: nativeRoomId, + }, + }, + }); + } + return roomId; +} + export async function ensureDMExists(client: MatrixClient, userId: string): Promise { const existingDMRoom = findDMForUser(client, userId); let roomId; @@ -310,6 +339,7 @@ export async function ensureDMExists(client: MatrixClient, userId: string): Prom if (privateShouldBeEncrypted()) { encryption = await canEncryptToAllUsers(client, [userId]); } + roomId = await createRoom({encryption, dmUserId: userId, spinner: false, andView: false}); await _waitForMember(client, roomId, userId); } diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index f420dd2b08..ea118a4c58 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -63,9 +63,6 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } this.emit(LISTS_UPDATE_EVENT); }); - // When new rooms arrive, we may hold them here until we have enough info to know whether we should before display them. - private roomHoldingPen: Room[] = []; - private holdNewRooms = false; private readonly watchedSettings = [ 'feature_custom_tags', @@ -129,24 +126,6 @@ export class RoomListStoreClass extends AsyncStoreWithClient { this.updateFn.trigger(); } - // After calling this, any new rooms that appear are not displayed until stopHoldingNewRooms() - // is called. Be sure to always call this in a try/finally block to ensure stopHoldingNewRooms - // is called afterwards. - public startHoldingNewRooms() { - console.log("hold-new-rooms mode enabled."); - this.holdNewRooms = true; - } - - public stopHoldingNewRooms() { - console.log("hold-new-rooms mode disabled: processing " + this.roomHoldingPen.length + " held rooms"); - this.holdNewRooms = false; - for (const heldRoom of this.roomHoldingPen) { - console.log("Processing held room: " + heldRoom.roomId); - this.handleRoomUpdate(heldRoom, RoomUpdateCause.NewRoom); - } - this.roomHoldingPen = []; - } - private checkLoggingEnabled() { if (SettingsStore.getValue("advancedRoomListLogging")) { console.warn("Advanced room list logging is enabled"); @@ -420,18 +399,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient { private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { if (cause === RoomUpdateCause.NewRoom) { - if (this.holdNewRooms) { - console.log("Room updates are held: putting room " + room.roomId + " into the holding pen"); - this.roomHoldingPen.push(room); - return; - } else { - // Let the visibility provider know that there is a new invited room. It would be nice - // if this could just be an event that things listen for but the point of this is that - // we delay doing anything about this room until the VoipUserMapper had had a chance - // to do the things it needs to do to decide if we should show this room or not, so - // an even wouldn't et us do that. - await VisibilityProvider.instance.onNewInvitedRoom(room); - } + // Let the visibility provider know that there is a new invited room. It would be nice + // if this could just be an event that things listen for but the point of this is that + // we delay doing anything about this room until the VoipUserMapper had had a chance + // to do the things it needs to do to decide if we should show this room or not, so + // an even wouldn't et us do that. + await VisibilityProvider.instance.onNewInvitedRoom(room); } if (!VisibilityProvider.instance.isRoomVisible(room)) { diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts index 2239f9e1ac..af38141e5d 100644 --- a/src/stores/room-list/filters/VisibilityProvider.ts +++ b/src/stores/room-list/filters/VisibilityProvider.ts @@ -42,7 +42,7 @@ export class VisibilityProvider { if ( CallHandler.sharedInstance().getSupportsVirtualRooms() && - VoipUserMapper.sharedInstance().isVirtualRoom(room.roomId) + VoipUserMapper.sharedInstance().isVirtualRoom(room) ) { isVisible = false; forced = true; From 648295e26b8b999f816a3ecedb3cd41328bfb39a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 17 Feb 2021 19:00:21 +0000 Subject: [PATCH 10/10] Oops, we still used this cache for new incoming virtual rooms --- src/VoipUserMapper.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index 5f4e33dc04..d919615349 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -24,6 +24,8 @@ import { Room } from 'matrix-js-sdk/src/models/room'; // is sip virtual: there could be others in the future. export default class VoipUserMapper { + private virtualRoomIdCache = new Set(); + public static sharedInstance(): VoipUserMapper { if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper(); return window.mxVoipUserMapper; @@ -61,6 +63,8 @@ export default class VoipUserMapper { public isVirtualRoom(room: Room):boolean { if (this.nativeRoomForVirtualRoom(room.roomId)) return true; + if (this.virtualRoomIdCache.has(room.roomId)) return true; + // also look in the create event for the claimed native room ID, which is the only // way we can recognise a virtual room we've created when it first arrives down // our stream. We don't trust this in general though, as it could be faked by an