/* Copyright 2024 New Vector Ltd. Copyright 2019-2022 The Matrix.org Foundation C.I.C. Copyright 2021 Šimon Brandner Copyright 2017, 2018 New Vector Ltd Copyright 2015, 2016 OpenMarket Ltd SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import React from "react"; import { MatrixError, RuleId, TweakName, SyncState } from "matrix-js-sdk/src/matrix"; import { CallError, CallErrorCode, CallEvent, CallParty, CallState, CallType, FALLBACK_ICE_SERVER, MatrixCall, } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; import EventEmitter from "events"; import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler"; import { MatrixClientPeg } from "./MatrixClientPeg"; import Modal from "./Modal"; import { _t } from "./languageHandler"; import dis from "./dispatcher/dispatcher"; import WidgetUtils from "./utils/WidgetUtils"; import SettingsStore from "./settings/SettingsStore"; import { WidgetType } from "./widgets/WidgetType"; import { SettingLevel } from "./settings/SettingLevel"; import QuestionDialog from "./components/views/dialogs/QuestionDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import WidgetStore from "./stores/WidgetStore"; import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore"; import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions"; import { UIFeature } from "./settings/UIFeature"; import { Action } from "./dispatcher/actions"; import VoipUserMapper from "./VoipUserMapper"; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from "./widgets/ManagedHybrid"; import SdkConfig from "./SdkConfig"; import { ensureDMExists } from "./createRoom"; import { Container, WidgetLayoutStore } from "./stores/widgets/WidgetLayoutStore"; import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from "./toasts/IncomingLegacyCallToast"; import ToastStore from "./stores/ToastStore"; import Resend from "./Resend"; import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; import { InviteKind } from "./components/views/dialogs/InviteDialogTypes"; import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogPayload"; import { findDMForUser } from "./utils/dm/findDMForUser"; import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers"; import { localNotificationsAreSilenced } from "./utils/notifications"; import { SdkContextClass } from "./contexts/SDKContext"; import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog"; import { isNotNull } from "./Typeguards"; import { BackgroundAudio } from "./audio/BackgroundAudio"; 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; type MediaEventType = keyof HTMLMediaElementEventMap; const MEDIA_ERROR_EVENT_TYPES: MediaEventType[] = [ "error", // The media has become empty; for example, this event is sent if the media has // already been loaded (or partially loaded), and the HTMLMediaElement.load method // is called to reload it. "emptied", // The user agent is trying to fetch media data, but data is unexpectedly not // forthcoming. "stalled", // Media data loading has been suspended. "suspend", // Playback has stopped because of a temporary lack of data "waiting", ]; const MEDIA_DEBUG_EVENT_TYPES: MediaEventType[] = [ "play", "pause", "playing", "ended", "loadeddata", "loadedmetadata", "canplay", "canplaythrough", "volumechange", ]; const MEDIA_EVENT_TYPES = [...MEDIA_ERROR_EVENT_TYPES, ...MEDIA_DEBUG_EVENT_TYPES]; export enum AudioID { Ring = "ringAudio", Ringback = "ringbackAudio", CallEnd = "callendAudio", Busy = "busyAudio", } /* istanbul ignore next */ const debuglog = (...args: any[]): void => { if (SettingsStore.getValue("debug_legacy_call_handler")) { logger.log.call(console, "LegacyCallHandler debuglog:", ...args); } }; 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: ThirdpartyLookupResponseFields; } export enum LegacyCallHandlerEvent { CallsChanged = "calls_changed", CallChangeRoom = "call_change_room", SilencedCallsChanged = "silenced_calls_changed", CallState = "call_state", } /** * LegacyCallHandler manages all currently active calls. It should be used for * placing, answering, rejecting and hanging up calls. It also handles ringing, * PSTN support and other things. */ export default class LegacyCallHandler extends EventEmitter { private calls = new Map(); // roomId -> call // Calls started as an attended transfer, ie. with the intention of transferring another // call with a different party to this one. private transferees = new Map(); // callId (target) -> call (transferee) private supportsPstnProtocol: boolean | null = null; private pstnSupportPrefixed: boolean | null = null; // True if the server only support the prefixed pstn protocol private supportsSipNativeVirtual: boolean | null = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native // Map of the asserted identity users after we've looked them up using the API. // We need to be be able to determine the mapped room synchronously, so we // do the async lookup when we get new information and then store these mappings here private assertedIdentityNativeUsers = new Map(); private silencedCalls = new Set(); // callIds private backgroundAudio = new BackgroundAudio(); private playingSources: Record = {}; // Record them for stopping public static get instance(): LegacyCallHandler { if (!window.mxLegacyCallHandler) { window.mxLegacyCallHandler = new LegacyCallHandler(); } return window.mxLegacyCallHandler; } /* * 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 roomIdForCall(call?: MatrixCall): string | null { if (!call) return null; // check asserted identity: if we're not obeying asserted identity, // this map will never be populated, but we check anyway for sanity if (this.shouldObeyAssertedfIdentity()) { const nativeUser = this.assertedIdentityNativeUsers.get(call.callId); if (nativeUser) { const room = findDMForUser(MatrixClientPeg.safeGet(), nativeUser); if (room) return room.roomId; } } return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) ?? call.roomId ?? null; } public start(): void { if (SettingsStore.getValue(UIFeature.Voip)) { MatrixClientPeg.safeGet().on(CallEventHandlerEvent.Incoming, this.onCallIncoming); } this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS); } public stop(): void { const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener(CallEventHandlerEvent.Incoming, this.onCallIncoming); } } /* istanbul ignore next (remove if we start using this function for things other than debug logging) */ public handleEvent(e: Event): void { const target = e.target as HTMLElement; const audioId = target?.id; if (MEDIA_ERROR_EVENT_TYPES.includes(e.type as MediaEventType)) { logger.error(`LegacyCallHandler: encountered "${e.type}" event with