UNPKG

matrix-js-sdk

Version:
1,187 lines (1,111 loc) 291 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.UNSTABLE_MSC3852_LAST_SEEN_UA = exports.RoomVersionStability = exports.PendingEventOrdering = exports.MatrixClient = exports.M_AUTHENTICATION = exports.ClientEvent = exports.CRYPTO_ENABLED = void 0; exports.fixNotificationCountOnDecryption = fixNotificationCountOnDecryption; var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _sync = require("./sync"); var _event = require("./models/event"); var _stub = require("./store/stub"); var _call = require("./webrtc/call"); var _filter = require("./filter"); var _callEventHandler = require("./webrtc/callEventHandler"); var utils = _interopRequireWildcard(require("./utils")); var _eventTimeline = require("./models/event-timeline"); var _pushprocessor = require("./pushprocessor"); var _autodiscovery = require("./autodiscovery"); var olmlib = _interopRequireWildcard(require("./crypto/olmlib")); var _ReEmitter = require("./ReEmitter"); var _RoomList = require("./crypto/RoomList"); var _logger = require("./logger"); var _serviceTypes = require("./service-types"); var _httpApi = require("./http-api"); var _crypto = require("./crypto"); var _recoverykey = require("./crypto/recoverykey"); var _key_passphrase = require("./crypto/key_passphrase"); var _user = require("./models/user"); var _contentRepo = require("./content-repo"); var _searchResult = require("./models/search-result"); var _dehydration = require("./crypto/dehydration"); var _api = require("./crypto/api"); var ContentHelpers = _interopRequireWildcard(require("./content-helpers")); var _room = require("./models/room"); var _roomMember = require("./models/room-member"); var _event2 = require("./@types/event"); var _partials = require("./@types/partials"); var _eventMapper = require("./event-mapper"); var _randomstring = require("./randomstring"); var _backup = require("./crypto/backup"); var _MSC3089TreeSpace = require("./models/MSC3089TreeSpace"); var _search = require("./@types/search"); var _PushRules = require("./@types/PushRules"); var _groupCall = require("./webrtc/groupCall"); var _mediaHandler = require("./webrtc/mediaHandler"); var _groupCallEventHandler = require("./webrtc/groupCallEventHandler"); var _typedEventEmitter = require("./models/typed-event-emitter"); var _read_receipts = require("./@types/read_receipts"); var _slidingSyncSdk = require("./sliding-sync-sdk"); var _thread = require("./models/thread"); var _beacon = require("./@types/beacon"); var _NamespacedValue = require("./NamespacedValue"); var _ToDeviceMessageQueue = require("./ToDeviceMessageQueue"); var _invitesIgnorer = require("./models/invites-ignorer"); var _feature = require("./feature"); var _constants = require("./rust-crypto/constants"); const _excluded = ["server", "limit", "since"]; function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } const SCROLLBACK_DELAY_MS = 3000; const CRYPTO_ENABLED = (0, _crypto.isCryptoAvailable)(); exports.CRYPTO_ENABLED = CRYPTO_ENABLED; const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes const UNSTABLE_MSC3852_LAST_SEEN_UA = new _NamespacedValue.UnstableValue("last_seen_user_agent", "org.matrix.msc3852.last_seen_user_agent"); exports.UNSTABLE_MSC3852_LAST_SEEN_UA = UNSTABLE_MSC3852_LAST_SEEN_UA; let PendingEventOrdering; exports.PendingEventOrdering = PendingEventOrdering; (function (PendingEventOrdering) { PendingEventOrdering["Chronological"] = "chronological"; PendingEventOrdering["Detached"] = "detached"; })(PendingEventOrdering || (exports.PendingEventOrdering = PendingEventOrdering = {})); let RoomVersionStability; exports.RoomVersionStability = RoomVersionStability; (function (RoomVersionStability) { RoomVersionStability["Stable"] = "stable"; RoomVersionStability["Unstable"] = "unstable"; })(RoomVersionStability || (exports.RoomVersionStability = RoomVersionStability = {})); var CrossSigningKeyType; (function (CrossSigningKeyType) { CrossSigningKeyType["MasterKey"] = "master_key"; CrossSigningKeyType["SelfSigningKey"] = "self_signing_key"; CrossSigningKeyType["UserSigningKey"] = "user_signing_key"; })(CrossSigningKeyType || (CrossSigningKeyType = {})); const M_AUTHENTICATION = new _NamespacedValue.UnstableValue("m.authentication", "org.matrix.msc2965.authentication"); exports.M_AUTHENTICATION = M_AUTHENTICATION; /* eslint-enable camelcase */ // We're using this constant for methods overloading and inspect whether a variable // contains an eventId or not. This was required to ensure backwards compatibility // of methods for threads // Probably not the most graceful solution but does a good enough job for now const EVENT_ID_PREFIX = "$"; let ClientEvent; exports.ClientEvent = ClientEvent; (function (ClientEvent) { ClientEvent["Sync"] = "sync"; ClientEvent["Event"] = "event"; ClientEvent["ToDeviceEvent"] = "toDeviceEvent"; ClientEvent["AccountData"] = "accountData"; ClientEvent["Room"] = "Room"; ClientEvent["DeleteRoom"] = "deleteRoom"; ClientEvent["SyncUnexpectedError"] = "sync.unexpectedError"; ClientEvent["ClientWellKnown"] = "WellKnown.client"; ClientEvent["ReceivedVoipEvent"] = "received_voip_event"; ClientEvent["UndecryptableToDeviceEvent"] = "toDeviceEvent.undecryptable"; ClientEvent["TurnServers"] = "turnServers"; ClientEvent["TurnServersError"] = "turnServers.error"; })(ClientEvent || (exports.ClientEvent = ClientEvent = {})); const SSO_ACTION_PARAM = new _NamespacedValue.UnstableValue("action", "org.matrix.msc3824.action"); /** * Represents a Matrix Client. Only directly construct this if you want to use * custom modules. Normally, {@link createClient} should be used * as it specifies 'sensible' defaults for these modules. */ class MatrixClient extends _typedEventEmitter.TypedEventEmitter { // populated after initCrypto // XXX: Intended private, used in code. // libolm crypto implementation. XXX: Intended private, used in code. Being replaced by cryptoBackend // one of crypto or rustCrypto // XXX: Intended private, used in code. // XXX: Intended private, used in code. // XXX: Intended private, used in code. // XXX: Intended private, used in code. // XXX: Intended private, used in code. // Note: these are all `protected` to let downstream consumers make mistakes if they want to. // We don't technically support this usage, but have reasons to do this. // The pushprocessor caches useful things, so keep one and re-use it // Promise to a response of the server's /versions response // TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020 // A manager for determining which invites should be ignored. constructor(opts) { var _opts$usingExternalCr; super(); (0, _defineProperty2.default)(this, "reEmitter", new _ReEmitter.TypedReEmitter(this)); (0, _defineProperty2.default)(this, "olmVersion", null); (0, _defineProperty2.default)(this, "usingExternalCrypto", false); (0, _defineProperty2.default)(this, "store", void 0); (0, _defineProperty2.default)(this, "deviceId", void 0); (0, _defineProperty2.default)(this, "credentials", void 0); (0, _defineProperty2.default)(this, "pickleKey", void 0); (0, _defineProperty2.default)(this, "scheduler", void 0); (0, _defineProperty2.default)(this, "clientRunning", false); (0, _defineProperty2.default)(this, "timelineSupport", false); (0, _defineProperty2.default)(this, "urlPreviewCache", {}); (0, _defineProperty2.default)(this, "identityServer", void 0); (0, _defineProperty2.default)(this, "http", void 0); (0, _defineProperty2.default)(this, "crypto", void 0); (0, _defineProperty2.default)(this, "cryptoBackend", void 0); (0, _defineProperty2.default)(this, "cryptoCallbacks", void 0); (0, _defineProperty2.default)(this, "callEventHandler", void 0); (0, _defineProperty2.default)(this, "groupCallEventHandler", void 0); (0, _defineProperty2.default)(this, "supportsCallTransfer", false); (0, _defineProperty2.default)(this, "forceTURN", false); (0, _defineProperty2.default)(this, "iceCandidatePoolSize", 0); (0, _defineProperty2.default)(this, "idBaseUrl", void 0); (0, _defineProperty2.default)(this, "baseUrl", void 0); (0, _defineProperty2.default)(this, "canSupportVoip", false); (0, _defineProperty2.default)(this, "peekSync", null); (0, _defineProperty2.default)(this, "isGuestAccount", false); (0, _defineProperty2.default)(this, "ongoingScrollbacks", {}); (0, _defineProperty2.default)(this, "notifTimelineSet", null); (0, _defineProperty2.default)(this, "cryptoStore", void 0); (0, _defineProperty2.default)(this, "verificationMethods", void 0); (0, _defineProperty2.default)(this, "fallbackICEServerAllowed", false); (0, _defineProperty2.default)(this, "roomList", void 0); (0, _defineProperty2.default)(this, "syncApi", void 0); (0, _defineProperty2.default)(this, "roomNameGenerator", void 0); (0, _defineProperty2.default)(this, "pushRules", void 0); (0, _defineProperty2.default)(this, "syncLeftRoomsPromise", void 0); (0, _defineProperty2.default)(this, "syncedLeftRooms", false); (0, _defineProperty2.default)(this, "clientOpts", void 0); (0, _defineProperty2.default)(this, "clientWellKnownIntervalID", void 0); (0, _defineProperty2.default)(this, "canResetTimelineCallback", void 0); (0, _defineProperty2.default)(this, "canSupport", new Map()); (0, _defineProperty2.default)(this, "pushProcessor", new _pushprocessor.PushProcessor(this)); (0, _defineProperty2.default)(this, "serverVersionsPromise", void 0); (0, _defineProperty2.default)(this, "cachedCapabilities", void 0); (0, _defineProperty2.default)(this, "clientWellKnown", void 0); (0, _defineProperty2.default)(this, "clientWellKnownPromise", void 0); (0, _defineProperty2.default)(this, "turnServers", []); (0, _defineProperty2.default)(this, "turnServersExpiry", 0); (0, _defineProperty2.default)(this, "checkTurnServersIntervalID", void 0); (0, _defineProperty2.default)(this, "exportedOlmDeviceToImport", void 0); (0, _defineProperty2.default)(this, "txnCtr", 0); (0, _defineProperty2.default)(this, "mediaHandler", new _mediaHandler.MediaHandler(this)); (0, _defineProperty2.default)(this, "sessionId", void 0); (0, _defineProperty2.default)(this, "pendingEventEncryption", new Map()); (0, _defineProperty2.default)(this, "useE2eForGroupCall", true); (0, _defineProperty2.default)(this, "toDeviceMessageQueue", void 0); (0, _defineProperty2.default)(this, "ignoredInvites", void 0); (0, _defineProperty2.default)(this, "startCallEventHandler", () => { if (this.isInitialSyncComplete()) { this.callEventHandler.start(); this.groupCallEventHandler.start(); this.off(ClientEvent.Sync, this.startCallEventHandler); } }); (0, _defineProperty2.default)(this, "fixupRoomNotifications", () => { if (this.isInitialSyncComplete()) { var _this$getRooms; const unreadRooms = ((_this$getRooms = this.getRooms()) !== null && _this$getRooms !== void 0 ? _this$getRooms : []).filter(room => { return room.getUnreadNotificationCount(_room.NotificationCountType.Total) > 0; }); for (const room of unreadRooms) { const currentUserId = this.getSafeUserId(); room.fixupNotifications(currentUserId); } this.off(ClientEvent.Sync, this.fixupRoomNotifications); } }); opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl); opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl); this.baseUrl = opts.baseUrl; this.idBaseUrl = opts.idBaseUrl; this.identityServer = opts.identityServer; this.usingExternalCrypto = (_opts$usingExternalCr = opts.usingExternalCrypto) !== null && _opts$usingExternalCr !== void 0 ? _opts$usingExternalCr : false; this.store = opts.store || new _stub.StubStore(); this.deviceId = opts.deviceId || null; this.sessionId = (0, _randomstring.randomString)(10); const userId = opts.userId || null; this.credentials = { userId }; this.http = new _httpApi.MatrixHttpApi(this, { fetchFn: opts.fetchFn, baseUrl: opts.baseUrl, idBaseUrl: opts.idBaseUrl, accessToken: opts.accessToken, prefix: _httpApi.ClientPrefix.R0, onlyData: true, extraParams: opts.queryParams, localTimeoutMs: opts.localTimeoutMs, useAuthorizationHeader: opts.useAuthorizationHeader }); if (opts.deviceToImport) { if (this.deviceId) { _logger.logger.warn("not importing device because device ID is provided to " + "constructor independently of exported data"); } else if (this.credentials.userId) { _logger.logger.warn("not importing device because user ID is provided to " + "constructor independently of exported data"); } else if (!opts.deviceToImport.deviceId) { _logger.logger.warn("not importing device because no device ID in exported data"); } else { this.deviceId = opts.deviceToImport.deviceId; this.credentials.userId = opts.deviceToImport.userId; // will be used during async initialization of the crypto this.exportedOlmDeviceToImport = opts.deviceToImport.olmDevice; } } else if (opts.pickleKey) { this.pickleKey = opts.pickleKey; } this.scheduler = opts.scheduler; if (this.scheduler) { this.scheduler.setProcessFunction(async eventToSend => { const room = this.getRoom(eventToSend.getRoomId()); if (eventToSend.status !== _event.EventStatus.SENDING) { this.updatePendingEventStatus(room, eventToSend, _event.EventStatus.SENDING); } const res = await this.sendEventHttpRequest(eventToSend); if (room) { // ensure we update pending event before the next scheduler run so that any listeners to event id // updates on the synchronous event emitter get a chance to run first. room.updatePendingEvent(eventToSend, _event.EventStatus.SENT, res.event_id); } return res; }); } if ((0, _call.supportsMatrixCall)()) { this.callEventHandler = new _callEventHandler.CallEventHandler(this); this.groupCallEventHandler = new _groupCallEventHandler.GroupCallEventHandler(this); this.canSupportVoip = true; // Start listening for calls after the initial sync is done // We do not need to backfill the call event buffer // with encrypted events that might never get decrypted this.on(ClientEvent.Sync, this.startCallEventHandler); } this.on(ClientEvent.Sync, this.fixupRoomNotifications); this.timelineSupport = Boolean(opts.timelineSupport); this.cryptoStore = opts.cryptoStore; this.verificationMethods = opts.verificationMethods; this.cryptoCallbacks = opts.cryptoCallbacks || {}; this.forceTURN = opts.forceTURN || false; this.iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize; this.supportsCallTransfer = opts.supportsCallTransfer || false; this.fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false; if (opts.useE2eForGroupCall !== undefined) this.useE2eForGroupCall = opts.useE2eForGroupCall; // List of which rooms have encryption enabled: separate from crypto because // we still want to know which rooms are encrypted even if crypto is disabled: // we don't want to start sending unencrypted events to them. this.roomList = new _RoomList.RoomList(this.cryptoStore); this.roomNameGenerator = opts.roomNameGenerator; this.toDeviceMessageQueue = new _ToDeviceMessageQueue.ToDeviceMessageQueue(this); // The SDK doesn't really provide a clean way for events to recalculate the push // actions for themselves, so we have to kinda help them out when they are encrypted. // We do this so that push rules are correctly executed on events in their decrypted // state, such as highlights when the user's name is mentioned. this.on(_event.MatrixEventEvent.Decrypted, event => { fixNotificationCountOnDecryption(this, event); }); // Like above, we have to listen for read receipts from ourselves in order to // correctly handle notification counts on encrypted rooms. // This fixes https://github.com/vector-im/element-web/issues/9421 this.on(_room.RoomEvent.Receipt, (event, room) => { if (room && this.isRoomEncrypted(room.roomId)) { // Figure out if we've read something or if it's just informational const content = event.getContent(); const isSelf = Object.keys(content).filter(eid => { for (const [key, value] of Object.entries(content[eid])) { if (!utils.isSupportedReceiptType(key)) continue; if (!value) continue; if (Object.keys(value).includes(this.getUserId())) return true; } return false; }).length > 0; if (!isSelf) return; // Work backwards to determine how many events are unread. We also set // a limit for how back we'll look to avoid spinning CPU for too long. // If we hit the limit, we assume the count is unchanged. const maxHistory = 20; const events = room.getLiveTimeline().getEvents(); let highlightCount = 0; for (let i = events.length - 1; i >= 0; i--) { var _pushActions$tweaks; if (i === events.length - maxHistory) return; // limit reached const event = events[i]; if (room.hasUserReadEvent(this.getUserId(), event.getId())) { // If the user has read the event, then the counting is done. break; } const pushActions = this.getPushActionsForEvent(event); highlightCount += pushActions !== null && pushActions !== void 0 && (_pushActions$tweaks = pushActions.tweaks) !== null && _pushActions$tweaks !== void 0 && _pushActions$tweaks.highlight ? 1 : 0; } // Note: we don't need to handle 'total' notifications because the counts // will come from the server. room.setUnreadNotificationCount(_room.NotificationCountType.Highlight, highlightCount); } }); this.ignoredInvites = new _invitesIgnorer.IgnoredInvites(this); } /** * High level helper method to begin syncing and poll for new events. To listen for these * events, add a listener for {@link ClientEvent.Event} * via {@link MatrixClient#on}. Alternatively, listen for specific * state change events. * @param opts - Options to apply when syncing. */ async startClient(opts) { var _opts; if (this.clientRunning) { // client is already running. return; } this.clientRunning = true; // backwards compat for when 'opts' was 'historyLen'. if (typeof opts === "number") { opts = { initialSyncLimit: opts }; } // Create our own user object artificially (instead of waiting for sync) // so it's always available, even if the user is not in any rooms etc. const userId = this.getUserId(); if (userId) { this.store.storeUser(new _user.User(userId)); } // periodically poll for turn servers if we support voip if (this.canSupportVoip) { this.checkTurnServersIntervalID = setInterval(() => { this.checkTurnServers(); }, TURN_CHECK_INTERVAL); // noinspection ES6MissingAwait this.checkTurnServers(); } if (this.syncApi) { // This shouldn't happen since we thought the client was not running _logger.logger.error("Still have sync object whilst not running: stopping old one"); this.syncApi.stop(); } try { await this.getVersions(); // This should be done with `canSupport` // TODO: https://github.com/vector-im/element-web/issues/23643 const { threads, list, fwdPagination } = await this.doesServerSupportThread(); _thread.Thread.setServerSideSupport(threads); _thread.Thread.setServerSideListSupport(list); _thread.Thread.setServerSideFwdPaginationSupport(fwdPagination); } catch (e) { _logger.logger.error("Can't fetch server versions, continuing to initialise sync, this will be retried later", e); } this.clientOpts = (_opts = opts) !== null && _opts !== void 0 ? _opts : {}; if (this.clientOpts.slidingSync) { this.syncApi = new _slidingSyncSdk.SlidingSyncSdk(this.clientOpts.slidingSync, this, this.clientOpts, this.buildSyncApiOptions()); } else { this.syncApi = new _sync.SyncApi(this, this.clientOpts, this.buildSyncApiOptions()); } if (this.clientOpts.hasOwnProperty("experimentalThreadSupport")) { _logger.logger.warn("`experimentalThreadSupport` has been deprecated, use `threadSupport` instead"); } // If `threadSupport` is omitted and the deprecated `experimentalThreadSupport` has been passed // We should fallback to that value for backwards compatibility purposes if (!this.clientOpts.hasOwnProperty("threadSupport") && this.clientOpts.hasOwnProperty("experimentalThreadSupport")) { this.clientOpts.threadSupport = this.clientOpts.experimentalThreadSupport; } this.syncApi.sync(); if (this.clientOpts.clientWellKnownPollPeriod !== undefined) { this.clientWellKnownIntervalID = setInterval(() => { this.fetchClientWellKnown(); }, 1000 * this.clientOpts.clientWellKnownPollPeriod); this.fetchClientWellKnown(); } this.toDeviceMessageQueue.start(); } /** * Construct a SyncApiOptions for this client, suitable for passing into the SyncApi constructor */ buildSyncApiOptions() { return { crypto: this.crypto, cryptoCallbacks: this.cryptoBackend, canResetEntireTimeline: roomId => { if (!this.canResetTimelineCallback) { return false; } return this.canResetTimelineCallback(roomId); } }; } /** * High level helper method to stop the client from polling and allow a * clean shutdown. */ stopClient() { var _this$cryptoBackend, _this$syncApi, _this$peekSync, _this$callEventHandle, _this$groupCallEventH; (_this$cryptoBackend = this.cryptoBackend) === null || _this$cryptoBackend === void 0 ? void 0 : _this$cryptoBackend.stop(); // crypto might have been initialised even if the client wasn't fully started if (!this.clientRunning) return; // already stopped _logger.logger.log("stopping MatrixClient"); this.clientRunning = false; (_this$syncApi = this.syncApi) === null || _this$syncApi === void 0 ? void 0 : _this$syncApi.stop(); this.syncApi = undefined; (_this$peekSync = this.peekSync) === null || _this$peekSync === void 0 ? void 0 : _this$peekSync.stopPeeking(); (_this$callEventHandle = this.callEventHandler) === null || _this$callEventHandle === void 0 ? void 0 : _this$callEventHandle.stop(); (_this$groupCallEventH = this.groupCallEventHandler) === null || _this$groupCallEventH === void 0 ? void 0 : _this$groupCallEventH.stop(); this.callEventHandler = undefined; this.groupCallEventHandler = undefined; global.clearInterval(this.checkTurnServersIntervalID); this.checkTurnServersIntervalID = undefined; if (this.clientWellKnownIntervalID !== undefined) { global.clearInterval(this.clientWellKnownIntervalID); } this.toDeviceMessageQueue.stop(); } /** * Try to rehydrate a device if available. The client must have been * initialized with a `cryptoCallback.getDehydrationKey` option, and this * function must be called before initCrypto and startClient are called. * * @returns Promise which resolves to undefined if a device could not be dehydrated, or * to the new device ID if the dehydration was successful. * @returns Rejects: with an error response. */ async rehydrateDevice() { if (this.crypto) { throw new Error("Cannot rehydrate device after crypto is initialized"); } if (!this.cryptoCallbacks.getDehydrationKey) { return; } const getDeviceResult = await this.getDehydratedDevice(); if (!getDeviceResult) { return; } if (!getDeviceResult.device_data || !getDeviceResult.device_id) { _logger.logger.info("no dehydrated device found"); return; } const account = new global.Olm.Account(); try { const deviceData = getDeviceResult.device_data; if (deviceData.algorithm !== _dehydration.DEHYDRATION_ALGORITHM) { _logger.logger.warn("Wrong algorithm for dehydrated device"); return; } _logger.logger.log("unpickling dehydrated device"); const key = await this.cryptoCallbacks.getDehydrationKey(deviceData, k => { // copy the key so that it doesn't get clobbered account.unpickle(new Uint8Array(k), deviceData.account); }); account.unpickle(key, deviceData.account); _logger.logger.log("unpickled device"); const rehydrateResult = await this.http.authedRequest(_httpApi.Method.Post, "/dehydrated_device/claim", undefined, { device_id: getDeviceResult.device_id }, { prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2" }); if (rehydrateResult.success) { this.deviceId = getDeviceResult.device_id; _logger.logger.info("using dehydrated device"); const pickleKey = this.pickleKey || "DEFAULT_KEY"; this.exportedOlmDeviceToImport = { pickledAccount: account.pickle(pickleKey), sessions: [], pickleKey: pickleKey }; account.free(); return this.deviceId; } else { account.free(); _logger.logger.info("not using dehydrated device"); return; } } catch (e) { account.free(); _logger.logger.warn("could not unpickle", e); } } /** * Get the current dehydrated device, if any * @returns A promise of an object containing the dehydrated device */ async getDehydratedDevice() { try { return await this.http.authedRequest(_httpApi.Method.Get, "/dehydrated_device", undefined, undefined, { prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2" }); } catch (e) { _logger.logger.info("could not get dehydrated device", e); return; } } /** * Set the dehydration key. This will also periodically dehydrate devices to * the server. * * @param key - the dehydration key * @param keyInfo - Information about the key. Primarily for * information about how to generate the key from a passphrase. * @param deviceDisplayName - The device display name for the * dehydrated device. * @returns A promise that resolves when the dehydrated device is stored. */ async setDehydrationKey(key, keyInfo, deviceDisplayName) { if (!this.crypto) { _logger.logger.warn("not dehydrating device if crypto is not enabled"); return; } return this.crypto.dehydrationManager.setKeyAndQueueDehydration(key, keyInfo, deviceDisplayName); } /** * Creates a new dehydrated device (without queuing periodic dehydration) * @param key - the dehydration key * @param keyInfo - Information about the key. Primarily for * information about how to generate the key from a passphrase. * @param deviceDisplayName - The device display name for the * dehydrated device. * @returns the device id of the newly created dehydrated device */ async createDehydratedDevice(key, keyInfo, deviceDisplayName) { if (!this.crypto) { _logger.logger.warn("not dehydrating device if crypto is not enabled"); return; } await this.crypto.dehydrationManager.setKey(key, keyInfo, deviceDisplayName); return this.crypto.dehydrationManager.dehydrateDevice(); } async exportDevice() { if (!this.crypto) { _logger.logger.warn("not exporting device if crypto is not enabled"); return; } return { userId: this.credentials.userId, deviceId: this.deviceId, // XXX: Private member access. olmDevice: await this.crypto.olmDevice.export() }; } /** * Clear any data out of the persistent stores used by the client. * * @returns Promise which resolves when the stores have been cleared. */ clearStores() { if (this.clientRunning) { throw new Error("Cannot clear stores while client is running"); } const promises = []; promises.push(this.store.deleteAllData()); if (this.cryptoStore) { promises.push(this.cryptoStore.deleteAllData()); } // delete the stores used by the rust matrix-sdk-crypto, in case they were used const deleteRustSdkStore = async () => { let indexedDB; try { indexedDB = global.indexedDB; } catch (e) { // No indexeddb support return; } for (const dbname of [`${_constants.RUST_SDK_STORE_PREFIX}::matrix-sdk-crypto`, `${_constants.RUST_SDK_STORE_PREFIX}::matrix-sdk-crypto-meta`]) { const prom = new Promise((resolve, reject) => { _logger.logger.info(`Removing IndexedDB instance ${dbname}`); const req = indexedDB.deleteDatabase(dbname); req.onsuccess = _ => { _logger.logger.info(`Removed IndexedDB instance ${dbname}`); resolve(0); }; req.onerror = e => { // In private browsing, Firefox has a global.indexedDB, but attempts to delete an indexeddb // (even a non-existent one) fail with "DOMException: A mutation operation was attempted on a // database that did not allow mutations." // // it seems like the only thing we can really do is ignore the error. _logger.logger.warn(`Failed to remove IndexedDB instance ${dbname}:`, e); resolve(0); }; req.onblocked = e => { _logger.logger.info(`cannot yet remove IndexedDB instance ${dbname}`); }; }); await prom; } }; promises.push(deleteRustSdkStore()); return Promise.all(promises).then(); // .then to fix types } /** * Get the user-id of the logged-in user * * @returns MXID for the logged-in user, or null if not logged in */ getUserId() { if (this.credentials && this.credentials.userId) { return this.credentials.userId; } return null; } /** * Get the user-id of the logged-in user * * @returns MXID for the logged-in user * @throws Error if not logged in */ getSafeUserId() { const userId = this.getUserId(); if (!userId) { throw new Error("Expected logged in user but found none."); } return userId; } /** * Get the domain for this client's MXID * @returns Domain of this MXID */ getDomain() { if (this.credentials && this.credentials.userId) { return this.credentials.userId.replace(/^.*?:/, ""); } return null; } /** * Get the local part of the current user ID e.g. "foo" in "\@foo:bar". * @returns The user ID localpart or null. */ getUserIdLocalpart() { if (this.credentials && this.credentials.userId) { return this.credentials.userId.split(":")[0].substring(1); } return null; } /** * Get the device ID of this client * @returns device ID */ getDeviceId() { return this.deviceId; } /** * Get the session ID of this client * @returns session ID */ getSessionId() { return this.sessionId; } /** * Check if the runtime environment supports VoIP calling. * @returns True if VoIP is supported. */ supportsVoip() { return this.canSupportVoip; } /** * @returns */ getMediaHandler() { return this.mediaHandler; } /** * Set whether VoIP calls are forced to use only TURN * candidates. This is the same as the forceTURN option * when creating the client. * @param force - True to force use of TURN servers */ setForceTURN(force) { this.forceTURN = force; } /** * Set whether to advertise transfer support to other parties on Matrix calls. * @param support - True to advertise the 'm.call.transferee' capability */ setSupportsCallTransfer(support) { this.supportsCallTransfer = support; } /** * Returns true if to-device signalling for group calls will be encrypted with Olm. * If false, it will be sent unencrypted. * @returns boolean Whether group call signalling will be encrypted */ getUseE2eForGroupCall() { return this.useE2eForGroupCall; } /** * Creates a new call. * The place*Call methods on the returned call can be used to actually place a call * * @param roomId - The room the call is to be placed in. * @returns the call or null if the browser doesn't support calling. */ createCall(roomId) { return (0, _call.createNewMatrixCall)(this, roomId); } /** * Creates a new group call and sends the associated state event * to alert other members that the room now has a group call. * * @param roomId - The room the call is to be placed in. */ async createGroupCall(roomId, type, isPtt, intent, dataChannelsEnabled, dataChannelOptions) { if (this.getGroupCallForRoom(roomId)) { throw new Error(`${roomId} already has an existing group call`); } const room = this.getRoom(roomId); if (!room) { throw new Error(`Cannot find room ${roomId}`); } return new _groupCall.GroupCall(this, room, type, isPtt, intent, undefined, dataChannelsEnabled, dataChannelOptions).create(); } /** * Wait until an initial state for the given room has been processed by the * client and the client is aware of any ongoing group calls. Awaiting on * the promise returned by this method before calling getGroupCallForRoom() * avoids races where getGroupCallForRoom is called before the state for that * room has been processed. It does not, however, fix other races, eg. two * clients both creating a group call at the same time. * @param roomId - The room ID to wait for * @returns A promise that resolves once existing group calls in the room * have been processed. */ waitUntilRoomReadyForGroupCalls(roomId) { return this.groupCallEventHandler.waitUntilRoomReadyForGroupCalls(roomId); } /** * Get an existing group call for the provided room. * @returns The group call or null if it doesn't already exist. */ getGroupCallForRoom(roomId) { return this.groupCallEventHandler.groupCalls.get(roomId) || null; } /** * Get the current sync state. * @returns the sync state, which may be null. * @see MatrixClient#event:"sync" */ getSyncState() { var _this$syncApi$getSync, _this$syncApi2; return (_this$syncApi$getSync = (_this$syncApi2 = this.syncApi) === null || _this$syncApi2 === void 0 ? void 0 : _this$syncApi2.getSyncState()) !== null && _this$syncApi$getSync !== void 0 ? _this$syncApi$getSync : null; } /** * Returns the additional data object associated with * the current sync state, or null if there is no * such data. * Sync errors, if available, are put in the 'error' key of * this object. */ getSyncStateData() { if (!this.syncApi) { return null; } return this.syncApi.getSyncStateData(); } /** * Whether the initial sync has completed. * @returns True if at least one sync has happened. */ isInitialSyncComplete() { const state = this.getSyncState(); if (!state) { return false; } return state === _sync.SyncState.Prepared || state === _sync.SyncState.Syncing; } /** * Return whether the client is configured for a guest account. * @returns True if this is a guest access_token (or no token is supplied). */ isGuest() { return this.isGuestAccount; } /** * Set whether this client is a guest account. <b>This method is experimental * and may change without warning.</b> * @param guest - True if this is a guest account. */ setGuest(guest) { // EXPERIMENTAL: // If the token is a macaroon, it should be encoded in it that it is a 'guest' // access token, which means that the SDK can determine this entirely without // the dev manually flipping this flag. this.isGuestAccount = guest; } /** * Return the provided scheduler, if any. * @returns The scheduler or undefined */ getScheduler() { return this.scheduler; } /** * Retry a backed off syncing request immediately. This should only be used when * the user <b>explicitly</b> attempts to retry their lost connection. * Will also retry any outbound to-device messages currently in the queue to be sent * (retries of regular outgoing events are handled separately, per-event). * @returns True if this resulted in a request being retried. */ retryImmediately() { var _this$syncApi$retryIm, _this$syncApi3; // don't await for this promise: we just want to kick it off this.toDeviceMessageQueue.sendQueue(); return (_this$syncApi$retryIm = (_this$syncApi3 = this.syncApi) === null || _this$syncApi3 === void 0 ? void 0 : _this$syncApi3.retryImmediately()) !== null && _this$syncApi$retryIm !== void 0 ? _this$syncApi$retryIm : false; } /** * Return the global notification EventTimelineSet, if any * * @returns the globl notification EventTimelineSet */ getNotifTimelineSet() { return this.notifTimelineSet; } /** * Set the global notification EventTimelineSet * */ setNotifTimelineSet(set) { this.notifTimelineSet = set; } /** * Gets the capabilities of the homeserver. Always returns an object of * capability keys and their options, which may be empty. * @param fresh - True to ignore any cached values. * @returns Promise which resolves to the capabilities of the homeserver * @returns Rejects: with an error response. */ getCapabilities(fresh = false) { const now = new Date().getTime(); if (this.cachedCapabilities && !fresh) { if (now < this.cachedCapabilities.expiration) { _logger.logger.log("Returning cached capabilities"); return Promise.resolve(this.cachedCapabilities.capabilities); } } return this.http.authedRequest(_httpApi.Method.Get, "/capabilities").catch(e => { // We swallow errors because we need a default object anyhow _logger.logger.error(e); return {}; }).then((r = {}) => { const capabilities = r["capabilities"] || {}; // If the capabilities missed the cache, cache it for a shorter amount // of time to try and refresh them later. const cacheMs = Object.keys(capabilities).length ? CAPABILITIES_CACHE_MS : 60000 + Math.random() * 5000; this.cachedCapabilities = { capabilities, expiration: now + cacheMs }; _logger.logger.log("Caching capabilities: ", capabilities); return capabilities; }); } /** * Initialise support for end-to-end encryption in this client, using libolm. * * You should call this method after creating the matrixclient, but *before* * calling `startClient`, if you want to support end-to-end encryption. * * It will return a Promise which will resolve when the crypto layer has been * successfully initialised. */ async initCrypto() { if (!(0, _crypto.isCryptoAvailable)()) { throw new Error(`End-to-end encryption not supported in this js-sdk build: did ` + `you remember to load the olm library?`); } if (this.cryptoBackend) { _logger.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); return; } if (!this.cryptoStore) { // the cryptostore is provided by sdk.createClient, so this shouldn't happen throw new Error(`Cannot enable encryption: no cryptoStore provided`); } _logger.logger.log("Crypto: Starting up crypto store..."); await this.cryptoStore.startup(); // initialise the list of encrypted rooms (whether or not crypto is enabled) _logger.logger.log("Crypto: initialising roomlist..."); await this.roomList.init(); const userId = this.getUserId(); if (userId === null) { throw new Error(`Cannot enable encryption on MatrixClient with unknown userId: ` + `ensure userId is passed in createClient().`); } if (this.deviceId === null) { throw new Error(`Cannot enable encryption on MatrixClient with unknown deviceId: ` + `ensure deviceId is passed in createClient().`); } const crypto = new _crypto.Crypto(this, userId, this.deviceId, this.store, this.cryptoStore, this.roomList, this.verificationMethods); this.reEmitter.reEmit(crypto, [_crypto.CryptoEvent.KeyBackupFailed, _crypto.CryptoEvent.KeyBackupSessionsRemaining, _crypto.CryptoEvent.RoomKeyRequest, _crypto.CryptoEvent.RoomKeyRequestCancellation, _crypto.CryptoEvent.Warning, _crypto.CryptoEvent.DevicesUpdated, _crypto.CryptoEvent.WillUpdateDevices, _crypto.CryptoEvent.DeviceVerificationChanged, _crypto.CryptoEvent.UserTrustStatusChanged, _crypto.CryptoEvent.KeysChanged]); _logger.logger.log("Crypto: initialising crypto object..."); await crypto.init({ exportedOlmDevice: this.exportedOlmDeviceToImport, pickleKey: this.pickleKey }); delete this.exportedOlmDeviceToImport; this.olmVersion = _crypto.Crypto.getOlmVersion(); // if crypto initialisation was successful, tell it to attach its event handlers. crypto.registerEventHandlers(this); this.cryptoBackend = this.crypto = crypto; // upload our keys in the background this.crypto.uploadDeviceKeys().catch(e => { // TODO: throwing away this error is a really bad idea. _logger.logger.error("Error uploading device keys", e); }); } /** * Initialise support for end-to-end encryption in this client, using the rust matrix-sdk-crypto. * * An alternative to {@link initCrypto}. * * *WARNING*: this API is very experimental, should not be used in production, and may change without notice! * Eventually it will be deprecated and `initCrypto` will do the same thing. * * @experimental * * @returns a Promise which will resolve when the crypto layer has been * successfully initialised. */ async initRustCrypto() { if (this.cryptoBackend) { _logger.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); return; } const userId = this.getUserId(); if (userId === null) { throw new Error(`Cannot enable encryption on MatrixClient with unknown userId: ` + `ensure userId is passed in createClient().`); } const deviceId = this.getDeviceId(); if (deviceId === null) { throw new Error(`Cannot enable encryption on MatrixClient with unknown deviceId: ` + `ensure deviceId is passed in createClient().`); } // importing rust-crypto will download the webassembly, so we delay it until we know it will be // needed. const RustCrypto = await Promise.resolve().then(() => _interopRequireWildcard(require("./rust-crypto"))); const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId); this.cryptoBackend = rustCrypto; // attach the event listeners needed by RustCrypto this.on(_roomMember.RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto)); } /** * Is end-to-end crypto enabled for this client. * @returns True if end-to-end is enabled. */ isCryptoEnabled() { return !!this.cryptoBackend; } /** * Get the Ed25519 key for this device * * @returns base64-encoded ed25519 key. Null if crypto is * disabled. */ getDeviceEd25519Key() { var _this$crypto$getDevic, _this$crypto; return (_this$crypto$getDevic = (_this$crypto = this.crypto) === null || _this$crypto === void 0 ? void 0 : _this$crypto.getDeviceEd25519Key()) !== null && _this$crypto$getDevic !== void 0 ? _this$crypto$getDevic : null; } /** * Get the Curve25519 key for this device * * @returns base64-encoded curve25519 key. Null if crypto is * disabled. */ getDeviceCurve25519Key() { var _this$crypto$getDevic2, _this$crypto2; return (_this$crypto$getDevic2 = (_this$crypto2 = this.crypto) === null || _this$crypto2 === void 0 ? void 0 : _this$crypto2.getDeviceCurve25519Key()) !== null && _this$crypto$getDevic2 !== void 0 ? _this$crypto$getDevic2 : null; } /** * @deprecated Does nothing. */ async uploadKeys() { _logger.logger.warn("MatrixClient.uploadKeys is deprecated"); } /** * Download the keys for a list of users and stores the keys in the session * store. * @param userIds - The users to fetch. * @param forceDownload - Always download the keys even if cached. * * @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo} */ downloadKeys(userIds, forceDownload) { if (!this.crypto) { return Promise.reject(new Error("End-to-end encryption disabled")); } return this.crypto.downloadKeys(userIds, forceDownload); } /** * Get the stored device keys for a user id * * @param userId - the user to list keys for. * * @returns list of devices */ getStoredDevicesForUser(userId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.getStoredDevicesForUser(userId) || []; } /** * Get the stored device key for a user id and device id * * @param userId - the user to list keys for. * @param deviceId - unique identifier for the device * * @returns device or null */ getStoredDevice(userId, deviceId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.getStoredDevice(userId, deviceId) || null; } /** * Mark the given device as verified * * @param userId - owner of the device * @param deviceId - unique identifier for the device or user's * cross-signing public key ID. * * @param verified - whether to mark the device as verified. defaults * to 'true'. * * @returns * * @remarks * Fires {@link CryptoEvent#DeviceVerificationChanged} */ setDeviceVerified(userId, deviceId, verified = true) { const prom = this.setDeviceVerification(userId, deviceId, verified, null, null); // if one of the user's own devices is being marked as verified / unverified, // check the key backup status, since whether or not we use this depends on // whether it has a signature from a verified device if (userId == this.credentials.userId) { this.checkKeyBackup(); } return prom; } /** * Mark the given device as blocked/unblocked * * @param userId - owner of the device * @param deviceId - unique identifier for the device or user's * cross-signing public key ID. * * @param blocked - whether to mark the device as blocked. defaults * to 'true'. * * @returns * * @remarks * Fires {@link CryptoEvent.DeviceVerificationChanged} */ setDeviceBlocked(userId, deviceId, blocked = true) { return this.setDeviceVerification(userId, deviceId, null, blocked, null); } /** * Mark the given device as known/unknown * * @param userId - owner of the device * @param deviceId - unique identifier for the device or user's * cross-signing public key ID. * * @param known - whether to mark the device as known. defaults * to 'true'. * * @returns * * @remarks * Fires {@link CryptoEvent#DeviceVerificationChanged} */ setDeviceKnow