matrix-react-sdk
Version:
SDK for matrix.org using React
971 lines (943 loc) • 161 kB
JavaScript
"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