/* Copyright 2024 New Vector Ltd. Copyright 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import { Room, EventType } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { ensureVirtualRoomExists } from "./createRoom"; import { MatrixClientPeg } from "./MatrixClientPeg"; import DMRoomMap from "./utils/DMRoomMap"; import LegacyCallHandler from "./LegacyCallHandler"; import { VIRTUAL_ROOM_EVENT_TYPE } from "./call-types"; import { findDMForUser } from "./utils/dm/findDMForUser"; // Functions for mapping virtual users & rooms. Currently the only lookup // is sip virtual: there could be others in the future. export default class VoipUserMapper { // We store mappings of virtual -> native room IDs here until the local echo for the // account data arrives. private virtualToNativeRoomIdCache = new Map(); public static sharedInstance(): VoipUserMapper { if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper(); return window.mxVoipUserMapper; } private async userToVirtualUser(userId: string): Promise { const results = await LegacyCallHandler.instance.sipVirtualLookup(userId); if (results.length === 0 || !results[0].fields.lookup_success) return null; return results[0].userid; } private async getVirtualUserForRoom(roomId: string): Promise { const userId = DMRoomMap.shared().getUserIdForRoomId(roomId); if (!userId) return null; const virtualUser = await this.userToVirtualUser(userId); if (!virtualUser) return null; return virtualUser; } public async getOrCreateVirtualRoomForRoom(roomId: string): Promise { const virtualUser = await this.getVirtualUserForRoom(roomId); if (!virtualUser) return null; const cli = MatrixClientPeg.safeGet(); const virtualRoomId = await ensureVirtualRoomExists(cli, virtualUser, roomId); cli.setRoomAccountData(virtualRoomId!, VIRTUAL_ROOM_EVENT_TYPE, { native_room: roomId, }); this.virtualToNativeRoomIdCache.set(virtualRoomId!, roomId); return virtualRoomId; } /** * Gets the ID of the virtual room for a room, or null if the room has no * virtual room */ public async getVirtualRoomForRoom(roomId: string): Promise { const virtualUser = await this.getVirtualUserForRoom(roomId); if (!virtualUser) return undefined; return findDMForUser(MatrixClientPeg.safeGet(), virtualUser); } public nativeRoomForVirtualRoom(roomId: string): string | null { const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId); if (cachedNativeRoomId) { logger.log( "Returning native room ID " + cachedNativeRoomId + " for virtual room ID " + roomId + " from cache", ); return cachedNativeRoomId; } const cli = MatrixClientPeg.safeGet(); const virtualRoom = cli.getRoom(roomId); if (!virtualRoom) return null; const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null; const nativeRoomID = virtualRoomEvent.getContent()["native_room"]; const nativeRoom = cli.getRoom(nativeRoomID); if (!nativeRoom || nativeRoom.getMyMembership() !== KnownMembership.Join) return null; return nativeRoomID; } public isVirtualRoom(room: Room): boolean { if (this.nativeRoomForVirtualRoom(room.roomId)) return true; if (this.virtualToNativeRoomIdCache.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 // inviter: our main source of truth is the DM state. const roomCreateEvent = room.currentState.getStateEvents(EventType.RoomCreate, ""); 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.safeGet().getUserId()) return false; const claimedNativeRoomId = roomCreateEvent.getContent()[VIRTUAL_ROOM_EVENT_TYPE]; return Boolean(claimedNativeRoomId); } public async onNewInvitedRoom(invitedRoom: Room): Promise { if (!LegacyCallHandler.instance.getSupportsVirtualRooms()) return; const inviterId = invitedRoom.getDMInviter(); if (!inviterId) { logger.error("Could not find DM inviter for room id: " + invitedRoom.roomId); } logger.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); const result = await LegacyCallHandler.instance.sipNativeLookup(inviterId!); if (result.length === 0) { return; } if (result[0].fields.is_virtual) { const cli = MatrixClientPeg.safeGet(); const nativeUser = result[0].userid; const nativeRoom = findDMForUser(cli, 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. cli.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) cli.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.virtualToNativeRoomIdCache.set(invitedRoom.roomId, nativeRoom.roomId); } } } }