UNPKG

matrix-react-sdk

Version:
971 lines (943 loc) 161 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.PROTOCOL_SIP_VIRTUAL = exports.PROTOCOL_SIP_NATIVE = exports.PROTOCOL_PSTN_PREFIXED = exports.PROTOCOL_PSTN = exports.LegacyCallHandlerEvent = exports.AudioID = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireDefault(require("react")); var _matrix = require("matrix-js-sdk/src/matrix"); var _call = require("matrix-js-sdk/src/webrtc/call"); var _logger = require("matrix-js-sdk/src/logger"); var _events = _interopRequireDefault(require("events")); var _pushprocessor = require("matrix-js-sdk/src/pushprocessor"); var _callEventHandler = require("matrix-js-sdk/src/webrtc/callEventHandler"); var _MatrixClientPeg = require("./MatrixClientPeg"); var _Modal = _interopRequireDefault(require("./Modal")); var _languageHandler = require("./languageHandler"); var _dispatcher = _interopRequireDefault(require("./dispatcher/dispatcher")); var _WidgetUtils = _interopRequireDefault(require("./utils/WidgetUtils")); var _SettingsStore = _interopRequireDefault(require("./settings/SettingsStore")); var _WidgetType = require("./widgets/WidgetType"); var _SettingLevel = require("./settings/SettingLevel"); var _QuestionDialog = _interopRequireDefault(require("./components/views/dialogs/QuestionDialog")); var _ErrorDialog = _interopRequireDefault(require("./components/views/dialogs/ErrorDialog")); var _WidgetStore = _interopRequireDefault(require("./stores/WidgetStore")); var _WidgetMessagingStore = require("./stores/widgets/WidgetMessagingStore"); var _ElementWidgetActions = require("./stores/widgets/ElementWidgetActions"); var _UIFeature = require("./settings/UIFeature"); var _actions = require("./dispatcher/actions"); var _VoipUserMapper = _interopRequireDefault(require("./VoipUserMapper")); var _ManagedHybrid = require("./widgets/ManagedHybrid"); var _SdkConfig = _interopRequireDefault(require("./SdkConfig")); var _createRoom = require("./createRoom"); var _WidgetLayoutStore = require("./stores/widgets/WidgetLayoutStore"); var _IncomingLegacyCallToast = _interopRequireWildcard(require("./toasts/IncomingLegacyCallToast")); var _ToastStore = _interopRequireDefault(require("./stores/ToastStore")); var _Resend = _interopRequireDefault(require("./Resend")); var _InviteDialogTypes = require("./components/views/dialogs/InviteDialogTypes"); var _findDMForUser = require("./utils/dm/findDMForUser"); var _getJoinedNonFunctionalMembers = require("./utils/room/getJoinedNonFunctionalMembers"); var _notifications = require("./utils/notifications"); var _SDKContext = require("./contexts/SDKContext"); var _showCantStartACallDialog = require("./voice-broadcast/utils/showCantStartACallDialog"); var _Typeguards = require("./Typeguards"); var _BackgroundAudio = require("./audio/BackgroundAudio"); var _Jitsi = require("./widgets/Jitsi.ts"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } /* Copyright 2024 New Vector Ltd. Copyright 2019-2022 The Matrix.org Foundation C.I.C. Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com> 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. */ const PROTOCOL_PSTN = exports.PROTOCOL_PSTN = "m.protocol.pstn"; const PROTOCOL_PSTN_PREFIXED = exports.PROTOCOL_PSTN_PREFIXED = "im.vector.protocol.pstn"; const PROTOCOL_SIP_NATIVE = exports.PROTOCOL_SIP_NATIVE = "im.vector.protocol.sip_native"; const PROTOCOL_SIP_VIRTUAL = exports.PROTOCOL_SIP_VIRTUAL = "im.vector.protocol.sip_virtual"; const CHECK_PROTOCOLS_ATTEMPTS = 3; const MEDIA_ERROR_EVENT_TYPES = ["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 = ["play", "pause", "playing", "ended", "loadeddata", "loadedmetadata", "canplay", "canplaythrough", "volumechange"]; const MEDIA_EVENT_TYPES = [...MEDIA_ERROR_EVENT_TYPES, ...MEDIA_DEBUG_EVENT_TYPES]; let AudioID = exports.AudioID = /*#__PURE__*/function (AudioID) { AudioID["Ring"] = "ringAudio"; AudioID["Ringback"] = "ringbackAudio"; AudioID["CallEnd"] = "callendAudio"; AudioID["Busy"] = "busyAudio"; return AudioID; }({}); /* istanbul ignore next */ const debuglog = (...args) => { if (_SettingsStore.default.getValue("debug_legacy_call_handler")) { _logger.logger.log.call(console, "LegacyCallHandler debuglog:", ...args); } }; let LegacyCallHandlerEvent = exports.LegacyCallHandlerEvent = /*#__PURE__*/function (LegacyCallHandlerEvent) { LegacyCallHandlerEvent["CallsChanged"] = "calls_changed"; LegacyCallHandlerEvent["CallChangeRoom"] = "call_change_room"; LegacyCallHandlerEvent["SilencedCallsChanged"] = "silenced_calls_changed"; LegacyCallHandlerEvent["CallState"] = "call_state"; return LegacyCallHandlerEvent; }({}); /** * 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. */ class LegacyCallHandler extends _events.default { constructor(...args) { super(...args); (0, _defineProperty2.default)(this, "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. (0, _defineProperty2.default)(this, "transferees", new Map()); // callId (target) -> call (transferee) (0, _defineProperty2.default)(this, "supportsPstnProtocol", null); (0, _defineProperty2.default)(this, "pstnSupportPrefixed", null); // True if the server only support the prefixed pstn protocol (0, _defineProperty2.default)(this, "supportsSipNativeVirtual", 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 (0, _defineProperty2.default)(this, "assertedIdentityNativeUsers", new Map()); (0, _defineProperty2.default)(this, "silencedCalls", new Set()); // callIds (0, _defineProperty2.default)(this, "backgroundAudio", new _BackgroundAudio.BackgroundAudio()); (0, _defineProperty2.default)(this, "playingSources", {}); (0, _defineProperty2.default)(this, "onCallIncoming", call => { // if the runtime env doesn't do VoIP, stop here. if (!_MatrixClientPeg.MatrixClientPeg.get()?.supportsVoip()) { return; } const mappedRoomId = LegacyCallHandler.instance.roomIdForCall(call); if (!mappedRoomId) return; if (this.getCallForRoom(mappedRoomId)) { _logger.logger.log("Got incoming call for room " + mappedRoomId + " but there's already a call for this room: ignoring"); return; } this.addCallForRoom(mappedRoomId, call); this.setCallListeners(call); // Explicitly handle first state change this.onCallStateChanged(call.state, null, call); // get ready to send encrypted events in the room, so if the user does answer // the call, we'll be ready to send. NB. This is the protocol-level room ID not // the mapped one: that's where we'll send the events. const cli = _MatrixClientPeg.MatrixClientPeg.safeGet(); const room = cli.getRoom(call.roomId); if (room) cli.getCrypto()?.prepareToEncrypt(room); }); (0, _defineProperty2.default)(this, "onCallStateChanged", (newState, oldState, call) => { const mappedRoomId = this.roomIdForCall(call); if (!mappedRoomId || !this.matchesCallForThisRoom(call)) return; this.setCallState(call, newState); _dispatcher.default.dispatch({ action: "call_state", room_id: mappedRoomId, state: newState }); switch (oldState) { case _call.CallState.Ringing: this.pause(AudioID.Ring); break; case _call.CallState.InviteSent: this.pause(AudioID.Ringback); break; } if (newState !== _call.CallState.Ringing) { this.silencedCalls.delete(call.callId); } switch (newState) { case _call.CallState.Ringing: { const incomingCallPushRule = new _pushprocessor.PushProcessor(_MatrixClientPeg.MatrixClientPeg.safeGet()).getPushRuleById(_matrix.RuleId.IncomingCall); const pushRuleEnabled = incomingCallPushRule?.enabled; // actions can be either Tweaks | PushRuleActionName, ie an object or a string type enum // and we want to only run this check on the Tweaks const tweakSetToRing = incomingCallPushRule?.actions.some(action => typeof action !== "string" && action.set_tweak === _matrix.TweakName.Sound && action.value === "ring"); if (pushRuleEnabled && tweakSetToRing && !this.isForcedSilent()) { this.play(AudioID.Ring); } else { this.silenceCall(call.callId); } break; } case _call.CallState.InviteSent: { this.play(AudioID.Ringback); break; } case _call.CallState.Ended: { const hangupReason = call.hangupReason; if ((0, _Typeguards.isNotNull)(mappedRoomId)) { this.removeCallForRoom(mappedRoomId); } if (oldState === _call.CallState.InviteSent && call.hangupParty === _call.CallParty.Remote) { this.play(AudioID.Busy); // Don't show a modal when we got rejected/the call was hung up if (!hangupReason || [_call.CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) break; let title; let description; // TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...) if (call.hangupReason === _call.CallErrorCode.UserBusy) { title = (0, _languageHandler._t)("voip|user_busy"); description = (0, _languageHandler._t)("voip|user_busy_description"); } else { title = (0, _languageHandler._t)("voip|call_failed"); description = (0, _languageHandler._t)("voip|call_failed_description"); } _Modal.default.createDialog(_ErrorDialog.default, { title, description }); } else if (hangupReason === _call.CallErrorCode.AnsweredElsewhere && oldState === _call.CallState.Connecting) { _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("voip|answered_elsewhere"), description: (0, _languageHandler._t)("voip|answered_elsewhere_description") }); } else if (oldState !== _call.CallState.Fledgling && oldState !== _call.CallState.Ringing) { // don't play the end-call sound for calls that never got off the ground this.play(AudioID.CallEnd); } if ((0, _Typeguards.isNotNull)(mappedRoomId)) { this.logCallStats(call, mappedRoomId); } break; } } }); } // Record them for stopping static get instance() { 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) */ roomIdForCall(call) { 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 = (0, _findDMForUser.findDMForUser)(_MatrixClientPeg.MatrixClientPeg.safeGet(), nativeUser); if (room) return room.roomId; } } return _VoipUserMapper.default.sharedInstance().nativeRoomForVirtualRoom(call.roomId) ?? call.roomId ?? null; } start() { if (_SettingsStore.default.getValue(_UIFeature.UIFeature.Voip)) { _MatrixClientPeg.MatrixClientPeg.safeGet().on(_callEventHandler.CallEventHandlerEvent.Incoming, this.onCallIncoming); } this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS); } stop() { const cli = _MatrixClientPeg.MatrixClientPeg.get(); if (cli) { cli.removeListener(_callEventHandler.CallEventHandlerEvent.Incoming, this.onCallIncoming); } } /* istanbul ignore next (remove if we start using this function for things other than debug logging) */ handleEvent(e) { const target = e.target; const audioId = target?.id; if (MEDIA_ERROR_EVENT_TYPES.includes(e.type)) { _logger.logger.error(`LegacyCallHandler: encountered "${e.type}" event with <audio id="${audioId}">`, e); } else if (MEDIA_EVENT_TYPES.includes(e.type)) { debuglog(`encountered "${e.type}" event with <audio id="${audioId}">`, e); } } isForcedSilent() { const cli = _MatrixClientPeg.MatrixClientPeg.safeGet(); return (0, _notifications.localNotificationsAreSilenced)(cli); } silenceCall(callId) { if (!callId) return; this.silencedCalls.add(callId); this.emit(LegacyCallHandlerEvent.SilencedCallsChanged, this.silencedCalls); // Don't pause audio if we have calls which are still ringing if (this.areAnyCallsUnsilenced()) return; this.pause(AudioID.Ring); } unSilenceCall(callId) { if (!callId || this.isForcedSilent()) return; this.silencedCalls.delete(callId); this.emit(LegacyCallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.play(AudioID.Ring); } isCallSilenced(callId) { return this.isForcedSilent() || !!callId && this.silencedCalls.has(callId); } /** * Returns true if there is at least one unsilenced call * @returns {boolean} */ areAnyCallsUnsilenced() { for (const call of this.calls.values()) { if (call.state === _call.CallState.Ringing && !this.isCallSilenced(call.callId)) { return true; } } return false; } async checkProtocols(maxTries) { try { const protocols = await _MatrixClientPeg.MatrixClientPeg.safeGet().getThirdpartyProtocols(); 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; } _dispatcher.default.dispatch({ action: _actions.Action.PstnSupportUpdated }); if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) { this.supportsSipNativeVirtual = Boolean(protocols[PROTOCOL_SIP_NATIVE] && protocols[PROTOCOL_SIP_VIRTUAL]); } _dispatcher.default.dispatch({ action: _actions.Action.VirtualRoomSupportUpdated }); } catch (e) { if (maxTries === 1) { _logger.logger.log("Failed to check for protocol support and no retries remain: assuming no support", e); } else { _logger.logger.log("Failed to check for protocol support: will retry", e); window.setTimeout(() => { this.checkProtocols(maxTries - 1); }, 10000); } } } shouldObeyAssertedfIdentity() { return !!_SdkConfig.default.getObject("voip")?.get("obey_asserted_identity"); } getSupportsPstnProtocol() { return this.supportsPstnProtocol; } getSupportsVirtualRooms() { return this.supportsSipNativeVirtual; } async pstnLookup(phoneNumber) { try { return await _MatrixClientPeg.MatrixClientPeg.safeGet().getThirdpartyUser(this.pstnSupportPrefixed ? PROTOCOL_PSTN_PREFIXED : PROTOCOL_PSTN, { "m.id.phone": phoneNumber }); } catch (e) { _logger.logger.warn("Failed to lookup user from phone number", e); return Promise.resolve([]); } } async sipVirtualLookup(nativeMxid) { try { return await _MatrixClientPeg.MatrixClientPeg.safeGet().getThirdpartyUser(PROTOCOL_SIP_VIRTUAL, { native_mxid: nativeMxid }); } catch (e) { _logger.logger.warn("Failed to query SIP identity for user", e); return Promise.resolve([]); } } async sipNativeLookup(virtualMxid) { try { return await _MatrixClientPeg.MatrixClientPeg.safeGet().getThirdpartyUser(PROTOCOL_SIP_NATIVE, { virtual_mxid: virtualMxid }); } catch (e) { _logger.logger.warn("Failed to query identity for SIP user", e); return Promise.resolve([]); } } getCallById(callId) { for (const call of this.calls.values()) { if (call.callId === callId) return call; } return null; } getCallForRoom(roomId) { return this.calls.get(roomId) || null; } getAllActiveCalls() { const activeCalls = []; for (const call of this.calls.values()) { if (call.state !== _call.CallState.Ended && call.state !== _call.CallState.Ringing) { activeCalls.push(call); } } return activeCalls; } getAllActiveCallsNotInRoom(notInThisRoomId) { const callsNotInThatRoom = []; for (const [roomId, call] of this.calls.entries()) { if (roomId !== notInThisRoomId && call.state !== _call.CallState.Ended) { callsNotInThatRoom.push(call); } } return callsNotInThatRoom; } getAllActiveCallsForPip(roomId) { const room = _MatrixClientPeg.MatrixClientPeg.safeGet().getRoom(roomId); if (room && _WidgetLayoutStore.WidgetLayoutStore.instance.hasMaximisedWidget(room)) { // This checks if there is space for the call view in the aux panel // If there is no space any call should be displayed in PiP return this.getAllActiveCalls(); } return this.getAllActiveCallsNotInRoom(roomId); } getTransfereeForCallId(callId) { return this.transferees.get(callId); } async play(audioId) { const logPrefix = `LegacyCallHandler.play(${audioId}):`; _logger.logger.debug(`${logPrefix} beginning of function`); const audioInfo = { [AudioID.Ring]: [`./media/ring`, true], [AudioID.Ringback]: [`./media/ringback`, true], [AudioID.CallEnd]: [`./media/callend`, false], [AudioID.Busy]: [`./media/busy`, false] }; const [urlPrefix, loop] = audioInfo[audioId]; const source = await this.backgroundAudio.pickFormatAndPlay(urlPrefix, ["mp3", "ogg"], loop); this.playingSources[audioId] = source; _logger.logger.debug(`${logPrefix} playing audio successfully`); } pause(audioId) { const logPrefix = `LegacyCallHandler.pause(${audioId}):`; _logger.logger.debug(`${logPrefix} beginning of function`); const source = this.playingSources[audioId]; if (!source) { _logger.logger.debug(`${logPrefix} audio not playing`); return; } source.stop(); delete this.playingSources[audioId]; _logger.logger.debug(`${logPrefix} paused audio`); } /** * Returns whether the given audio is currently playing * Only supported for looping audio tracks * @param audioId the ID of the audio to query for playing state */ isPlaying(audioId) { return !!this.playingSources[audioId]; } matchesCallForThisRoom(call) { // We don't allow placing more than one call per room, but that doesn't mean there // can't be more than one, eg. in a glare situation. This checks that the given call // is the call we consider 'the' call for its room. const mappedRoomId = this.roomIdForCall(call); const callForThisRoom = mappedRoomId ? this.getCallForRoom(mappedRoomId) : null; return !!callForThisRoom && call.callId === callForThisRoom.callId; } setCallListeners(call) { let mappedRoomId = this.roomIdForCall(call); call.on(_call.CallEvent.Error, err => { if (!this.matchesCallForThisRoom(call)) return; _logger.logger.error("Call error:", err); if (err.code === _call.CallErrorCode.NoUserMedia) { this.showMediaCaptureError(call); return; } if (_MatrixClientPeg.MatrixClientPeg.safeGet().getTurnServers().length === 0 && _SettingsStore.default.getValue("fallbackICEServerAllowed") === null) { this.showICEFallbackPrompt(); return; } _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("voip|call_failed"), description: err.message }); }); call.on(_call.CallEvent.Hangup, () => { if (!mappedRoomId || !this.matchesCallForThisRoom(call)) return; if ((0, _Typeguards.isNotNull)(mappedRoomId)) { this.removeCallForRoom(mappedRoomId); } }); call.on(_call.CallEvent.State, (newState, oldState) => { this.onCallStateChanged(newState, oldState, call); }); call.on(_call.CallEvent.Replaced, newCall => { if (!mappedRoomId || !this.matchesCallForThisRoom(call)) return; _logger.logger.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`); if (call.state === _call.CallState.Ringing) { this.pause(AudioID.Ring); } else if (call.state === _call.CallState.InviteSent) { this.pause(AudioID.Ringback); } if ((0, _Typeguards.isNotNull)(mappedRoomId)) { this.removeCallForRoom(mappedRoomId); this.addCallForRoom(mappedRoomId, newCall); } this.setCallListeners(newCall); this.setCallState(newCall, newCall.state); }); call.on(_call.CallEvent.AssertedIdentityChanged, async () => { if (!mappedRoomId || !this.matchesCallForThisRoom(call)) return; _logger.logger.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity()); if (!this.shouldObeyAssertedfIdentity()) { _logger.logger.log("asserted identity not enabled in config: ignoring"); return; } const newAssertedIdentity = call.getRemoteAssertedIdentity()?.id; let newNativeAssertedIdentity = newAssertedIdentity; if (newAssertedIdentity) { const response = await this.sipNativeLookup(newAssertedIdentity); if (response.length && response[0].fields.lookup_success) { newNativeAssertedIdentity = response[0].userid; } } _logger.logger.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`); if (newNativeAssertedIdentity) { this.assertedIdentityNativeUsers.set(call.callId, newNativeAssertedIdentity); // If we don't already have a room with this user, make one. This will be slightly odd // if they called us because we'll be inviting them, but there's not much we can do about // this if we want the actual, native room to exist (which we do). This is why it's // important to only obey asserted identity in trusted environments, since anyone you're // on a call with can cause you to send a room invite to someone. await (0, _createRoom.ensureDMExists)(_MatrixClientPeg.MatrixClientPeg.safeGet(), newNativeAssertedIdentity); const newMappedRoomId = this.roomIdForCall(call); _logger.logger.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`); if (newMappedRoomId !== mappedRoomId && (0, _Typeguards.isNotNull)(mappedRoomId) && (0, _Typeguards.isNotNull)(newMappedRoomId)) { this.removeCallForRoom(mappedRoomId); mappedRoomId = newMappedRoomId; _logger.logger.log("Moving call to room " + mappedRoomId); this.addCallForRoom(mappedRoomId, call, true); } } }); } async logCallStats(call, mappedRoomId) { const stats = await call.getCurrentCallStats(); _logger.logger.debug(`Call completed. Call ID: ${call.callId}, virtual room ID: ${call.roomId}, ` + `user-facing room ID: ${mappedRoomId}, direction: ${call.direction}, ` + `our Party ID: ${call.ourPartyId}, hangup party: ${call.hangupParty}, ` + `hangup reason: ${call.hangupReason}`); if (!stats) { _logger.logger.debug("Call statistics are undefined. The call has probably failed before a peerConn was established"); return; } _logger.logger.debug("Local candidates:"); for (const cand of stats.filter(item => item.type === "local-candidate")) { const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip' _logger.logger.debug(`${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` + `protocol: ${cand.protocol}, relay protocol: ${cand.relayProtocol}, network type: ${cand.networkType}`); } _logger.logger.debug("Remote candidates:"); for (const cand of stats.filter(item => item.type === "remote-candidate")) { const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip' _logger.logger.debug(`${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` + `protocol: ${cand.protocol}`); } _logger.logger.debug("Candidate pairs:"); for (const pair of stats.filter(item => item.type === "candidate-pair")) { _logger.logger.debug(`${pair.localCandidateId} / ${pair.remoteCandidateId} - state: ${pair.state}, ` + `nominated: ${pair.nominated}, ` + `requests sent ${pair.requestsSent}, requests received ${pair.requestsReceived}, ` + `responses received: ${pair.responsesReceived}, responses sent: ${pair.responsesSent}, ` + `bytes received: ${pair.bytesReceived}, bytes sent: ${pair.bytesSent}, `); } _logger.logger.debug("Outbound RTP:"); for (const s of stats.filter(item => item.type === "outbound-rtp")) { _logger.logger.debug(s); } _logger.logger.debug("Inbound RTP:"); for (const s of stats.filter(item => item.type === "inbound-rtp")) { _logger.logger.debug(s); } } setCallState(call, status) { const mappedRoomId = LegacyCallHandler.instance.roomIdForCall(call); _logger.logger.log(`Call state in ${mappedRoomId} changed to ${status}`); const toastKey = (0, _IncomingLegacyCallToast.getIncomingLegacyCallToastKey)(call.callId); if (status === _call.CallState.Ringing) { _ToastStore.default.sharedInstance().addOrReplaceToast({ key: toastKey, priority: 100, component: _IncomingLegacyCallToast.default, bodyClassName: "mx_IncomingLegacyCallToast", props: { call } }); } else { _ToastStore.default.sharedInstance().dismissToast(toastKey); } this.emit(LegacyCallHandlerEvent.CallState, mappedRoomId, status); } removeCallForRoom(roomId) { _logger.logger.log("Removing call for room ", roomId); this.calls.delete(roomId); this.emit(LegacyCallHandlerEvent.CallsChanged, this.calls); } showICEFallbackPrompt() { const cli = _MatrixClientPeg.MatrixClientPeg.safeGet(); _Modal.default.createDialog(_QuestionDialog.default, { title: (0, _languageHandler._t)("voip|misconfigured_server"), description: /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("p", null, (0, _languageHandler._t)("voip|misconfigured_server_description", { homeserverDomain: cli.getDomain() }, { code: sub => /*#__PURE__*/_react.default.createElement("code", null, sub) })), /*#__PURE__*/_react.default.createElement("p", null, (0, _languageHandler._t)("voip|misconfigured_server_fallback", undefined, { server: () => /*#__PURE__*/_react.default.createElement("code", null, new URL(_call.FALLBACK_ICE_SERVER).pathname) }))), button: (0, _languageHandler._t)("voip|misconfigured_server_fallback_accept", { server: new URL(_call.FALLBACK_ICE_SERVER).pathname }), cancelButton: (0, _languageHandler._t)("action|ok"), onFinished: allow => { _SettingsStore.default.setValue("fallbackICEServerAllowed", null, _SettingLevel.SettingLevel.DEVICE, allow); } }, undefined, true); } showMediaCaptureError(call) { let title; let description; if (call.type === _call.CallType.Voice) { title = (0, _languageHandler._t)("voip|unable_to_access_microphone"); description = /*#__PURE__*/_react.default.createElement("div", null, (0, _languageHandler._t)("voip|call_failed_microphone")); } else if (call.type === _call.CallType.Video) { title = (0, _languageHandler._t)("voip|unable_to_access_media"); description = /*#__PURE__*/_react.default.createElement("div", null, (0, _languageHandler._t)("voip|call_failed_media"), /*#__PURE__*/_react.default.createElement("ul", null, /*#__PURE__*/_react.default.createElement("li", null, (0, _languageHandler._t)("voip|call_failed_media_connected")), /*#__PURE__*/_react.default.createElement("li", null, (0, _languageHandler._t)("voip|call_failed_media_permissions")), /*#__PURE__*/_react.default.createElement("li", null, (0, _languageHandler._t)("voip|call_failed_media_applications")))); } _Modal.default.createDialog(_ErrorDialog.default, { title, description }, undefined, true); } async placeMatrixCall(roomId, type, transferee) { const cli = _MatrixClientPeg.MatrixClientPeg.safeGet(); const mappedRoomId = (await _VoipUserMapper.default.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId; _logger.logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId); // If we're using a virtual room nd there are any events pending, try to resend them, // otherwise the call will fail and because its a virtual room, the user won't be able // to see it to either retry or clear the pending events. There will only be call events // in this queue, and since we're about to place a new call, they can only be events from // previous calls that are probably stale by now, so just cancel them. if (mappedRoomId !== roomId) { const mappedRoom = cli.getRoom(mappedRoomId); if (mappedRoom?.getPendingEvents().length) { _Resend.default.cancelUnsentEvents(mappedRoom); } } const timeUntilTurnCresExpire = cli.getTurnServersExpiry() - Date.now(); _logger.logger.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); const call = cli.createCall(mappedRoomId); try { this.addCallForRoom(roomId, call); } catch (e) { _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("voip|already_in_call"), description: (0, _languageHandler._t)("voip|already_in_call_person") }); return; } if (transferee) { this.transferees.set(call.callId, transferee); } this.setCallListeners(call); this.setActiveCallRoomId(roomId); if (type === _call.CallType.Voice) { call.placeVoiceCall(); } else if (type === "video") { call.placeVideoCall(); } else { _logger.logger.error("Unknown conf call type: " + type); } } async placeCall(roomId, type, transferee) { const cli = _MatrixClientPeg.MatrixClientPeg.safeGet(); const room = cli.getRoom(roomId); if (!room) { _logger.logger.error(`Room ${roomId} does not exist.`); return; } // Pause current broadcast, if any _SDKContext.SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.pause(); if (_SDKContext.SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()) { // Do not start a call, if recording a broadcast (0, _showCantStartACallDialog.showCantStartACallDialog)(); return; } // We might be using managed hybrid widgets if ((0, _ManagedHybrid.isManagedHybridWidgetEnabled)(room)) { await (0, _ManagedHybrid.addManagedHybridWidget)(room); return; } // if the runtime env doesn't do VoIP, whine. if (!cli.supportsVoip()) { _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("voip|unsupported"), description: (0, _languageHandler._t)("voip|unsupported_browser") }); return; } if (cli.getSyncState() === _matrix.SyncState.Error) { _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("voip|connection_lost"), description: (0, _languageHandler._t)("voip|connection_lost_description") }); return; } // don't allow > 2 calls to be placed. if (this.getAllActiveCalls().length > 1) { _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("voip|too_many_calls"), description: (0, _languageHandler._t)("voip|too_many_calls_description") }); return; } // We leave the check for whether there's already a call in this room until later, // otherwise it can race. const members = (0, _getJoinedNonFunctionalMembers.getJoinedNonFunctionalMembers)(room); if (members.length <= 1) { _Modal.default.createDialog(_ErrorDialog.default, { description: (0, _languageHandler._t)("voip|cannot_call_yourself_description") }); } else if (members.length === 2 && !_Jitsi.Jitsi.getInstance().useFor1To1Calls) { _logger.logger.info(`Place ${type} call in ${roomId}`); await this.placeMatrixCall(roomId, type, transferee); } else { // > 2 || useFor1To1Calls await this.placeJitsiCall(roomId, type); } } hangupAllCalls() { for (const call of this.calls.values()) { this.stopRingingIfPossible(call.callId); call.hangup(_call.CallErrorCode.UserHangup, false); } } hangupOrReject(roomId, reject) { const call = this.calls.get(roomId); // no call to hangup if (!call) return; this.stopRingingIfPossible(call.callId); if (reject) { call.reject(); } else { call.hangup(_call.CallErrorCode.UserHangup, false); } // don't remove the call yet: let the hangup event handler do it (otherwise it will throw // the hangup event away) } answerCall(roomId) { // no call to answer if (!this.calls.has(roomId)) return; const call = this.calls.get(roomId); this.stopRingingIfPossible(call.callId); if (this.getAllActiveCalls().length > 1) { _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("voip|too_many_calls"), description: (0, _languageHandler._t)("voip|too_many_calls_description") }); return; } call.answer(); this.setActiveCallRoomId(roomId); _dispatcher.default.dispatch({ action: _actions.Action.ViewRoom, room_id: roomId, metricsTrigger: "WebAcceptCall" }); } stopRingingIfPossible(callId) { this.silencedCalls.delete(callId); if (this.areAnyCallsUnsilenced()) return; this.pause(AudioID.Ring); } async dialNumber(number, transferee) { const results = await this.pstnLookup(number); if (!results || results.length === 0 || !results[0].userid) { _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("voip|msisdn_lookup_failed"), description: (0, _languageHandler._t)("voip|msisdn_lookup_failed_description") }); return; } const userId = results[0].userid; // Now check to see if this is a virtual user, in which case we should find the // native user let nativeUserId; if (this.getSupportsVirtualRooms()) { const nativeLookupResults = await this.sipNativeLookup(userId); const lookupSuccess = nativeLookupResults.length > 0 && nativeLookupResults[0].fields.lookup_success; nativeUserId = lookupSuccess ? nativeLookupResults[0].userid : userId; _logger.logger.log("Looked up " + number + " to " + userId + " and mapped to native user " + nativeUserId); } else { nativeUserId = userId; } const roomId = await (0, _createRoom.ensureDMExists)(_MatrixClientPeg.MatrixClientPeg.safeGet(), nativeUserId); if (!roomId) { throw new Error("Failed to ensure DM exists for dialing number"); } _dispatcher.default.dispatch({ action: _actions.Action.ViewRoom, room_id: roomId, metricsTrigger: "WebDialPad" }); await this.placeMatrixCall(roomId, _call.CallType.Voice, transferee); } async startTransferToPhoneNumber(call, destination, consultFirst) { if (consultFirst) { // if we're consulting, we just start by placing a call to the transfer // target (passing the transferee so the actual transfer can happen later) this.dialNumber(destination, call); return; } const results = await this.pstnLookup(destination); if (!results || results.length === 0 || !results[0].userid) { _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("voip|msisdn_transfer_failed"), description: (0, _languageHandler._t)("voip|msisdn_lookup_failed_description") }); return; } await this.startTransferToMatrixID(call, results[0].userid, consultFirst); } async startTransferToMatrixID(call, destination, consultFirst) { if (consultFirst) { const dmRoomId = await (0, _createRoom.ensureDMExists)(_MatrixClientPeg.MatrixClientPeg.safeGet(), destination); if (!dmRoomId) { _logger.logger.log("Failed to transfer call, could not ensure dm exists"); _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("voip|transfer_failed"), description: (0, _languageHandler._t)("voip|transfer_failed_description") }); return; } this.placeCall(dmRoomId, call.type, call); _dispatcher.default.dispatch({ action: _actions.Action.ViewRoom, room_id: dmRoomId, should_peek: false, joining: false, metricsTrigger: undefined // other }); } else { try { await call.transfer(destination); } catch (e) { _logger.logger.log("Failed to transfer call", e); _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("voip|transfer_failed"), description: (0, _languageHandler._t)("voip|transfer_failed_description") }); } } } setActiveCallRoomId(activeCallRoomId) { _logger.logger.info("Setting call in room " + activeCallRoomId + " active"); for (const [roomId, call] of this.calls.entries()) { if (call.state === _call.CallState.Ended) continue; if (roomId === activeCallRoomId) { call.setRemoteOnHold(false); } else { _logger.logger.info("Holding call in room " + roomId + " because another call is being set active"); call.setRemoteOnHold(true); } } } /** * @returns true if we are currently in any call where we haven't put the remote party on hold */ hasAnyUnheldCall() { for (const call of this.calls.values()) { if (call.state === _call.CallState.Ended) continue; if (!call.isRemoteOnHold()) return true; } return false; } async placeJitsiCall(roomId, type) { const client = _MatrixClientPeg.MatrixClientPeg.safeGet(); _logger.logger.info(`Place conference call in ${roomId}`); _dispatcher.default.dispatch({ action: "appsDrawer", show: true }); // Prevent double clicking the call button const widget = _WidgetStore.default.instance.getApps(roomId).find(app => _WidgetType.WidgetType.JITSI.matches(app.type)); if (widget) { // If there already is a Jitsi widget, pin it const room = client.getRoom(roomId); if ((0, _Typeguards.isNotNull)(room)) { _WidgetLayoutStore.WidgetLayoutStore.instance.moveToContainer(room, widget, _WidgetLayoutStore.Container.Top); } return; } try { await _WidgetUtils.default.addJitsiWidget(client, roomId, type, "Jitsi", false); _logger.logger.log("Jitsi widget added"); } catch (e) { if (e instanceof _matrix.MatrixError && e.errcode === "M_FORBIDDEN") { _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("voip|no_permission_conference"), description: (0, _languageHandler._t)("voip|no_permission_conference_description") }); } _logger.logger.error(e); } } hangupCallApp(roomId) { _logger.logger.info("Leaving conference call in " + roomId); const roomInfo = _WidgetStore.default.instance.getRoom(roomId); if (!roomInfo) return; // "should never happen" clauses go here const jitsiWidgets = roomInfo.widgets.filter(w => _WidgetType.WidgetType.JITSI.matches(w.type)); jitsiWidgets.forEach(w => { const messaging = _WidgetMessagingStore.WidgetMessagingStore.instance.getMessagingForUid(_WidgetUtils.default.getWidgetUid(w)); if (!messaging) return; // more "should never happen" words messaging.transport.send(_ElementWidgetActions.ElementWidgetActions.HangupCall, {}); }); } /* * Shows the transfer dialog for a call, signalling to the other end that * a transfer is about to happen */ showTransferDialog(call) { call.setRemoteOnHold(true); _dispatcher.default.dispatch({ action: _actions.Action.OpenInviteDialog, kind: _InviteDialogTypes.InviteKind.CallTransfer, call, analyticsName: "Transfer Call", className: "mx_InviteDialog_transferWrapper", onFinishedCallback: results => { if (results.length === 0 || results[0] === false) { call.setRemoteOnHold(false); } } }); } addCallForRoom(roomId, call, changedRooms = false) { if (this.calls.has(roomId)) { _logger.logger.log(`Couldn't add call to room ${roomId}: already have a call for this room`); throw new Error("Already have a call for room " + roomId); } _logger.logger.log("setting call for room " + roomId); this.calls.set(roomId, call); // Should we always emit CallsChanged too? if (changedRooms) { this.emit(LegacyCallHandlerEvent.CallChangeRoom, call); } else { this.emit(LegacyCallHandlerEvent.CallsChanged, this.calls); } } } exports.default = LegacyCallHandler; //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJfcmVhY3QiLCJfaW50ZXJvcFJlcXVpcmVEZWZhdWx0IiwicmVxdWlyZSIsIl9tYXRyaXgiLCJfY2FsbCIsIl9sb2dnZXIiLCJfZXZlbnRzIiwiX3B1c2hwcm9jZXNzb3IiLCJfY2FsbEV2ZW50SGFuZGxlciIsIl9NYXRyaXhDbGllbnRQZWciLCJfTW9kYWwiLCJfbGFuZ3VhZ2VIYW5kbGVyIiwiX2Rpc3BhdGNoZXIiLCJfV2lkZ2V0VXRpbHMiLCJfU2V0dGluZ3NTdG9yZSIsIl9XaWRnZXRUeXBlIiwiX1NldHRpbmdMZXZlbCIsIl9RdWVzdGlvbkRpYWxvZyIsIl9FcnJvckRpYWxvZyIsIl9XaWRnZXRTdG9yZSIsIl9XaWRnZXRNZXNzYWdpbmdTdG9yZSIsIl9FbGVtZW50V2lkZ2V0QWN0aW9ucyIsIl9VSUZlYXR1cmUiLCJfYWN0aW9ucyIsIl9Wb2lwVXNlck1hcHBlciIsIl9NYW5hZ2VkSHlicmlkIiwiX1Nka0NvbmZpZyIsIl9jcmVhdGVSb29tIiwiX1dpZGdldExheW91dFN0b3JlIiwiX0luY29taW5nTGVnYWN5Q2FsbFRvYXN0IiwiX2ludGVyb3BSZXF1aXJlV2lsZGNhcmQiLCJfVG9hc3RTdG9yZSIsIl9SZXNlbmQiLCJfSW52aXRlRGlhbG9nVHlwZXMiLCJfZmluZERNRm9yVXNlciIsIl9nZXRKb2luZWROb25GdW5jdGlvbmFsTWVtYmVycyIsIl9ub3RpZmljYXRpb25zIiwiX1NES0NvbnRleHQiLCJfc2hvd0NhbnRTdGFydEFDYWxsRGlhbG9nIiwiX1R5cGVndWFyZHMiLCJfQmFja2dyb3VuZEF1ZGlvIiwiX0ppdHNpIiwiX2dldFJlcXVpcmVXaWxkY2FyZENhY2hlIiwiZSIsIldlYWtNYXAiLCJyIiwidCIsIl9fZXNNb2R1bGUiLCJkZWZhdWx0IiwiaGFzIiwiZ2V0IiwibiIsIl9fcHJvdG9fXyIsImEiLCJPYmplY3QiLCJkZWZpbmVQcm9wZXJ0eSIsImdldE93blByb3BlcnR5RGVzY3JpcHRvciIsInUiLCJoYXNPd25Qcm9wZXJ0eSIsImNhbGwiLCJpIiwic2V0IiwiUFJPVE9DT0xfUFNUTiIsImV4cG9ydHMiLCJQUk9UT0NPTF9QU1ROX1BSRUZJWEVEIiwiUFJPVE9DT0xfU0lQX05BVElWRSIsIlBST1RPQ09MX1NJUF9WSVJUVUFMIiwiQ0hFQ0tfUFJPVE9DT0xTX0FUVEVNUFRTIiwiTUVESUFfRVJST1JfRVZFTlRfVFlQRVMiLCJNRURJQV9ERUJVR19FVkVOVF9UWVBFUyIsIk1FRElBX0VWRU5UX1RZUEVTIiwiQXVkaW9JRCIsImRlYnVnbG9nIiwiYXJncyIsIlNldHRpbmdzU3RvcmUiLCJnZXRWYWx1ZSIsImxvZ2dlciIsImxvZyIsImNvbnNvbGUiLCJMZWdhY3lDYWxsSGFuZGxlckV2ZW50IiwiTGVnYWN5Q2FsbEhhbmRsZXIiLCJFdmVudEVtaXR0ZXIiLCJjb25zdHJ1Y3RvciIsIl9kZWZpbmVQcm9wZXJ0eTIiLCJNYXAiLCJTZXQiLCJCYWNrZ3JvdW5kQXVkaW8iLCJNYXRyaXhDbGllbnRQZWciLCJzdXBwb3J0c1ZvaXAiLCJtYXBwZWRSb29tSWQiLCJpbnN0YW5jZSIsInJvb21JZEZvckNhbGwiLCJnZXRDYWxsRm9yUm9vbSIsImFkZENhbGxGb3JSb29tIiwic2V0Q2FsbExpc3RlbmVycyIsIm9uQ2FsbFN0YXRlQ2hhbmdlZCIsInN0YXRlIiwiY2xpIiwic2FmZUdldCIsInJvb20iLCJnZXRSb29tIiwicm9vbUlkIiwiZ2V0Q3J5cHRvIiwicHJlcGFyZVRvRW5jcnlwdCIsIm5ld1N0YXRlIiwib2xkU3RhdGUiLCJtYXRjaGVzQ2FsbEZvclRoaXNSb29tIiwic2V0Q2FsbFN0YXRlIiwiZGlzIiwiZGlzcGF0Y2giLCJhY3Rpb24iLCJyb29tX2lkIiwiQ2FsbFN0YXRlIiwiUmluZ2luZyIsInBhdXNlIiwiUmluZyIsIkludml0ZVNlbnQiLCJSaW5nYmFjayIsInNpbGVuY2VkQ2FsbHMiLCJkZWxldGUiLCJjYWxsSWQiLCJpbmNvbWluZ0NhbGxQdXNoUnVsZSIsIlB1c2hQcm9jZXNzb3IiLCJnZXRQdXNoUnVsZUJ5SWQiLCJSdWxlSWQiLCJJbmNvbWluZ0NhbGwiLCJwdXNoUnVsZUVuYWJsZWQiLCJlbmFibGVkIiwidHdlYWtTZXRUb1JpbmciLCJhY3Rpb25zIiwic29tZSIsInNldF90d2VhayIsIlR3ZWFrTmFtZSIsIlNvdW5kIiwidmFsdWUiLCJpc0ZvcmNlZFNpbGVudCIsInBsYXkiLCJzaWxlbmNlQ2FsbCIsIkVuZGVkIiwiaGFuZ3VwUmVhc29uIiwiaXNOb3ROdWxsIiwicmVtb3ZlQ2FsbEZvclJvb20iLCJoYW5ndXBQYXJ0eSIsIkNhbGxQYXJ0eSIsIlJlbW90ZSIsIkJ1c3kiLCJDYWxsRXJyb3JDb2RlIiwiVXNlckhhbmd1cCIsImluY2x1ZGVzIiwidGl0bGUiLCJkZXNjcmlwdGlvbiIsIlVzZXJCdXN5IiwiX3QiLCJNb2RhbCIsImNyZWF0ZURpYWxvZyIsIkVycm9yRGlhbG9nIiwiQW5zd2VyZWRFbHNld2hlcmUiLCJDb25uZWN0aW5nIiwiRmxlZGdsaW5nIiwiQ2FsbEVuZCIsImxvZ0NhbGxTdGF0cyIsIndpbmRvdyIsIm14TGVnYWN5Q2FsbEhhbmRsZXIiLCJzaG91bGRPYmV5QXNzZXJ0ZWRmSWRlbnRpdHkiLCJuYXRpdmVVc2VyIiwiYXNzZXJ0ZWRJZGVudGl0eU5hdGl2ZVVzZXJzIiwiZmluZERNRm9yVXNlciIsIlZvaXBVc2VyTWFwcGVyIiwic2hhcmVkSW5zdGFuY2UiLCJuYXRpdmVSb29tRm9yVmlydHVhbFJvb20iLCJzdGFydCIsIlVJRmVhdHVyZSIsIlZvaXAiLCJvbiIsIkNhbGxFdmVudEhhbmRsZXJFdmVudCIsIkluY29taW5nIiwib25DYWxsSW5jb21pbmciLCJjaGVja1Byb3RvY29scyIsInN0b3AiLCJyZW1vdmVMaXN0ZW5lciIsImhhbmRsZUV2ZW50IiwidGFyZ2V0IiwiYXVkaW9JZCIsImlkIiwidHlwZSIsImVycm9yIiwibG9jYWxOb3RpZmljYXRpb25zQXJlU2lsZW5jZWQiLCJhZGQiLCJlbWl0IiwiU2lsZW5jZWRDYWxsc0NoYW5nZWQiLCJhcmVBbnlDYWxsc1Vuc2lsZW5jZWQiLCJ1blNpbGVuY2VDYWxsIiwiaXNDYWxsU2lsZW5jZWQiLCJjYWxscyIsInZhbHVlcyIsIm1heFRyaWVzIiwicHJvdG9jb2xzIiwiZ2V0VGhpcmRwYXJ0eVByb3RvY29scyIsInVuZGVmaW5lZCIsInN1cHBvcnRzUHN0blByb3RvY29sIiwiQm9vbGVhbiIsInBzdG5TdXBwb3J0UHJlZml4ZWQiLCJBY3Rpb24iLCJQc3RuU3VwcG9ydFVwZGF0ZWQiLCJzdXBwb3J0c1NpcE5hdGl2ZVZpcnR1YWwiLCJWaXJ0dWFsUm9vbVN1cHBvcnRVcGRhdGVkIiwic2V0VGltZW91dCIsIlNka0NvbmZpZyIsImdldE9iamVjdCIsImdldFN1cHBvcnRzUHN0blByb3RvY29sIiwiZ2V0U3VwcG9ydHNWaXJ0dWFsUm9vbXMiLCJwc3RuTG9va3VwIiwicGhvbmVOdW1iZXIiLCJnZXRUaGlyZHBhcnR5VXNlciIsIndhcm4iLCJQcm9taXNlIiwicmVzb2x2ZSIsInNpcFZpcnR1YWxMb29rdXAiLCJuYXRpdmVNeGlkIiwibmF0aXZlX214aWQiLCJzaXBOYXRpdmVMb29rdXAiLCJ2aXJ0dWFsTXhpZCIsInZpcnR1YWxfbXhpZCIsImdldENhbGxCeUlkIiwiZ2V0QWxsQWN0aXZlQ2FsbHMiLCJhY3RpdmVDYWxscyIsInB1c2giLCJnZXRBbGxBY3RpdmVDYWxsc05vdEluUm9vbSIsIm5vdEluVGhpc1Jvb21JZCIsImNhbGxzTm90SW5UaGF0Um9vbSIsImVudHJpZXMiLCJnZXRBbGxBY3RpdmVDYWxsc0ZvclBpcCIsIldpZGdldExheW91dFN0b3JlIiwiaGFzTWF4aW1pc2VkV2lkZ2V0IiwiZ2V0VHJhbnNmZXJlZUZvckNhbGxJZCIsInRyYW5zZmVyZWVzIiwibG9nUHJlZml4IiwiZGVidWciLCJhdWRpb0luZm8iLCJ1cmxQcmVmaXgiLCJsb29wIiwic291cmNlIiwiYmFja2dyb3VuZEF1ZGlvIiwicGlja0Zvcm1hdEFuZFBsYXkiLCJwbGF5aW5nU291cmNlcyIsImlzUGxheWluZyIsImNhbGxGb3JUaGlzUm9vbSIsIkNhbGxFdmVudCIsIkVycm9yIiwiZXJyIiwiY29kZSIsIk5vVXNlck1lZGlhIiwic2hvd01lZGlhQ2FwdHVyZUVycm9yIiwiZ2V0VHVyblNlcnZlcnMiLCJsZW5ndGgiLCJzaG93SUNFRmFsbGJhY2tQcm9tcHQiLCJtZXNzYWdlIiwiSGFuZ3VwIiwiU3RhdGUiLCJSZXBsYWNlZCIsIm5ld0NhbGwiLCJBc3NlcnRlZElkZW50aXR5Q2hhbmdlZCIsImdldFJlbW90ZUFzc2VydGVkSWRlbnRpdHkiLCJuZXdBc3NlcnRlZElkZW50aXR5IiwibmV3TmF0aXZlQXNzZXJ0ZWRJZGVudGl0eSIsInJlc3BvbnNlIiwiZmllbGRzIiwibG9va3VwX3N1Y2Nlc3MiLCJ1c2VyaWQiLCJlbnN1cmVETUV4aXN0cyIsIm5ld01hcHBlZFJvb21JZCIsInN0YXRzIiwiZ2V0Q3VycmVudENhbGxTdGF0cyIsImRpcmVjdGlvbiIsIm91clBhcnR5SWQiLCJjYW5kIiwiZmlsdGVyIiwiaXRlbSIsImFkZHJlc3MiLCJpcCIsImNhbmRpZGF0ZVR5cGUiLCJwb3J0IiwicHJvdG9jb2wiLCJyZWxheVByb3RvY29sIiwibmV0d29ya1R5cGUiLCJwYWlyIiwibG9jYWxDYW5kaWRhdGVJZCIsInJlbW90ZUNhbmRpZGF0ZUlkIiwibm9taW5hdGVkIiwicmVxdWVzdHNTZW50IiwicmVxdWVzdHNSZWNlaXZlZCIsInJlc3BvbnNlc1JlY2VpdmVkIiwicmVzcG9uc2VzU2VudCIsImJ5dGVzUmVjZWl2ZWQiLCJieXRlc1NlbnQiLCJzIiwic3RhdHVzIiwidG9hc3RLZXkiLCJnZXRJbmNvbWluZ0xlZ2FjeUNhbGxUb2FzdEtleSIsIlRvYXN0U3RvcmUiLCJhZGRPclJlcGxhY2VUb2FzdCIsImtleSIsInByaW9yaXR5IiwiY29tcG9uZW50IiwiSW5jb21pbmdMZWdhY3lDYWxsVG9hc3QiLCJib2R5Q2xhc3NOYW1lIiwicHJvcHMiLCJkaXNtaXNzVG9hc3QiLCJDYWxsc0NoYW5nZWQiLCJRdWVzdGlvbkRpYWxvZyIsImNyZWF0ZUVsZW1lbnQiLCJob21lc2VydmVyRG9tYWluIiwiZ2V