UNPKG

matrix-js-sdk

Version:
1,197 lines (1,121 loc) 307 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.UNSTABLE_MSC3852_LAST_SEEN_UA = exports.UNSTABLE_MSC2666_SHARED_ROOMS = exports.UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = exports.UNSTABLE_MSC2666_MUTUAL_ROOMS = exports.RoomVersionStability = exports.PendingEventOrdering = exports.MatrixClient = exports.GET_LOGIN_TOKEN_CAPABILITY = exports.ClientEvent = exports.CRYPTO_ENABLED = void 0; exports.fixNotificationCountOnDecryption = fixNotificationCountOnDecryption; exports.inMainTimelineForReceipt = inMainTimelineForReceipt; exports.threadIdForReceipt = threadIdForReceipt; 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 _groupCallEventHandler = require("./webrtc/groupCallEventHandler"); var _utils = _interopRequireWildcard(require("./utils")); var utils = _utils; var _eventTimeline = require("./models/event-timeline"); var _pushprocessor = require("./pushprocessor"); var _autodiscovery = require("./autodiscovery"); var olmlib = _interopRequireWildcard(require("./crypto/olmlib")); var _base = require("./base64"); var _ReEmitter = require("./ReEmitter"); 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 _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"); var _secretStorage = require("./secret-storage"); var _MatrixRTCSessionManager = require("./matrixrtc/MatrixRTCSessionManager"); var _threadUtils = require("./thread-utils"); var _membership = require("./@types/membership"); const _excluded = ["server", "limit", "since"]; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } 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 2015-2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /** * This is an internal module. See {@link MatrixClient} for the public class. */ const SCROLLBACK_DELAY_MS = 3000; const CRYPTO_ENABLED = exports.CRYPTO_ENABLED = (0, _crypto.isCryptoAvailable)(); 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 = exports.UNSTABLE_MSC3852_LAST_SEEN_UA = new _NamespacedValue.UnstableValue("last_seen_user_agent", "org.matrix.msc3852.last_seen_user_agent"); let PendingEventOrdering = exports.PendingEventOrdering = /*#__PURE__*/function (PendingEventOrdering) { PendingEventOrdering["Chronological"] = "chronological"; PendingEventOrdering["Detached"] = "detached"; return PendingEventOrdering; }({}); let RoomVersionStability = exports.RoomVersionStability = /*#__PURE__*/function (RoomVersionStability) { RoomVersionStability["Stable"] = "stable"; RoomVersionStability["Unstable"] = "unstable"; return RoomVersionStability; }({}); const GET_LOGIN_TOKEN_CAPABILITY = exports.GET_LOGIN_TOKEN_CAPABILITY = new _NamespacedValue.NamespacedValue("m.get_login_token", "org.matrix.msc3882.get_login_token"); const UNSTABLE_MSC2666_SHARED_ROOMS = exports.UNSTABLE_MSC2666_SHARED_ROOMS = "uk.half-shot.msc2666"; const UNSTABLE_MSC2666_MUTUAL_ROOMS = exports.UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms"; const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = exports.UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_mutual_rooms"; /** * A representation of the capabilities advertised by a homeserver as defined by * [Capabilities negotiation](https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv3capabilities). */ /** @deprecated prefer {@link CrossSigningKeyInfo}. */ var CrossSigningKeyType = /*#__PURE__*/function (CrossSigningKeyType) { CrossSigningKeyType["MasterKey"] = "master_key"; CrossSigningKeyType["SelfSigningKey"] = "self_signing_key"; CrossSigningKeyType["UserSigningKey"] = "user_signing_key"; return CrossSigningKeyType; }(CrossSigningKeyType || {}); // Re-export for backwards compatibility /* 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 = /*#__PURE__*/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"; return 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 { // A manager for determining which invites should be ignored. constructor(opts) { var _opts$logger, _opts$usingExternalCr, _opts$cryptoCallbacks; super(); // If a custom logger is provided, use it. Otherwise, default to the global // one in logger.ts. (0, _defineProperty2.default)(this, "reEmitter", new _ReEmitter.TypedReEmitter(this)); (0, _defineProperty2.default)(this, "olmVersion", null); // populated after initCrypto (0, _defineProperty2.default)(this, "usingExternalCrypto", false); (0, _defineProperty2.default)(this, "clientRunning", false); (0, _defineProperty2.default)(this, "timelineSupport", false); (0, _defineProperty2.default)(this, "urlPreviewCache", {}); (0, _defineProperty2.default)(this, "supportsCallTransfer", false); // XXX: Intended private, used in code. (0, _defineProperty2.default)(this, "forceTURN", false); // XXX: Intended private, used in code. (0, _defineProperty2.default)(this, "iceCandidatePoolSize", 0); // 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. (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, "fallbackICEServerAllowed", false); (0, _defineProperty2.default)(this, "syncedLeftRooms", false); (0, _defineProperty2.default)(this, "canSupport", new Map()); // The pushprocessor caches useful things, so keep one and re-use it (0, _defineProperty2.default)(this, "pushProcessor", new _pushprocessor.PushProcessor(this)); (0, _defineProperty2.default)(this, "turnServers", []); (0, _defineProperty2.default)(this, "turnServersExpiry", 0); (0, _defineProperty2.default)(this, "txnCtr", 0); (0, _defineProperty2.default)(this, "mediaHandler", new _mediaHandler.MediaHandler(this)); /** IDs of events which are currently being encrypted. * * This is part of the cancellation mechanism: if the event is no longer listed here when encryption completes, * that tells us that it has been cancelled, and we should not send it. */ (0, _defineProperty2.default)(this, "eventsBeingEncrypted", new Set()); (0, _defineProperty2.default)(this, "useE2eForGroupCall", true); (0, _defineProperty2.default)(this, "startCallEventHandler", () => { if (this.isInitialSyncComplete()) { if ((0, _call.supportsMatrixCall)()) { this.callEventHandler.start(); this.groupCallEventHandler.start(); } this.off(ClientEvent.Sync, this.startCallEventHandler); } }); (0, _defineProperty2.default)(this, "startMatrixRTC", () => { if (this.isInitialSyncComplete()) { this.matrixRTC.start(); this.off(ClientEvent.Sync, this.startMatrixRTC); } }); /** * Once the client has been initialised, we want to clear notifications we * know for a fact should be here. * This issue should also be addressed on synapse's side and is tracked as part * of https://github.com/matrix-org/synapse/issues/14837 * * We consider a room or a thread as fully read if the current user has sent * the last event in the live timeline of that context and if the read receipt * we have on record matches. */ (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); } }); this.logger = (_opts$logger = opts.logger) !== null && _opts$logger !== void 0 ? _opts$logger : _logger.logger; 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, refreshToken: opts.refreshToken, tokenRefreshFunction: opts.tokenRefreshFunction, prefix: _httpApi.ClientPrefix.V3, onlyData: true, extraParams: opts.queryParams, localTimeoutMs: opts.localTimeoutMs, useAuthorizationHeader: opts.useAuthorizationHeader, logger: this.logger }); if (opts.deviceToImport) { if (this.deviceId) { this.logger.warn("not importing device because device ID is provided to " + "constructor independently of exported data"); } else if (this.credentials.userId) { this.logger.warn("not importing device because user ID is provided to " + "constructor independently of exported data"); } else if (!opts.deviceToImport.deviceId) { this.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.useLivekitForGroupCalls = Boolean(opts.useLivekitForGroupCalls); 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); } // NB. We initialise MatrixRTC whether we have call support or not: this is just // the underlying session management and doesn't use any actual media capabilities this.matrixRTC = new _MatrixRTCSessionManager.MatrixRTCSessionManager(this); 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; this.isVoipWithNoMediaAllowed = opts.isVoipWithNoMediaAllowed || false; if (opts.useE2eForGroupCall !== undefined) this.useE2eForGroupCall = opts.useE2eForGroupCall; this.livekitServiceURL = opts.livekitServiceURL; 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); }); this.ignoredInvites = new _invitesIgnorer.IgnoredInvites(this); this._secretStorage = new _secretStorage.ServerSideSecretStorageImpl(this, (_opts$cryptoCallbacks = opts.cryptoCallbacks) !== null && _opts$cryptoCallbacks !== void 0 ? _opts$cryptoCallbacks : {}); // having lots of event listeners is not unusual. 0 means "unlimited". this.setMaxListeners(0); } set store(newStore) { this._store = newStore; this._store.setUserCreator(userId => _user.User.createUser(userId, this)); } get store() { return this._store; } /** * 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; this.on(ClientEvent.Sync, this.startMatrixRTC); // 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 this.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) { this.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()); } this.syncApi.sync().catch(e => this.logger.info("Sync startup aborted with an error:", e)); 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 || _this$cryptoBackend.stop(); // crypto might have been initialised even if the client wasn't fully started this.off(ClientEvent.Sync, this.startMatrixRTC); if (!this.clientRunning) return; // already stopped this.logger.debug("stopping MatrixClient"); this.clientRunning = false; (_this$syncApi = this.syncApi) === null || _this$syncApi === void 0 || _this$syncApi.stop(); this.syncApi = undefined; (_this$peekSync = this.peekSync) === null || _this$peekSync === void 0 || _this$peekSync.stopPeeking(); (_this$callEventHandle = this.callEventHandler) === null || _this$callEventHandle === void 0 || _this$callEventHandle.stop(); (_this$groupCallEventH = this.groupCallEventHandler) === null || _this$groupCallEventH === 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(); this.matrixRTC.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) { this.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) { this.logger.warn("Wrong algorithm for dehydrated device"); return; } this.logger.debug("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); this.logger.debug("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; this.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(); this.logger.info("not using dehydrated device"); return; } } catch (e) { account.free(); this.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) { this.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) { this.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) { this.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) { this.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; if (!indexedDB) return; // No indexedDB support } 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) => { this.logger.info(`Removing IndexedDB instance ${dbname}`); const req = indexedDB.deleteDatabase(dbname); req.onsuccess = _ => { this.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. this.logger.warn(`Failed to remove IndexedDB instance ${dbname}:`, e); resolve(0); }; req.onblocked = e => { this.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}`); } // Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a // no media WebRTC connection anyway. return new _groupCall.GroupCall(this, room, type, isPtt, intent, undefined, dataChannelsEnabled || this.isVoipWithNoMediaAllowed, dataChannelOptions, this.isVoipWithNoMediaAllowed, this.useLivekitForGroupCalls, this.livekitServiceURL).create(); } getLivekitServiceURL() { return this.livekitServiceURL; } // This shouldn't need to exist, but the widget API has startup ordering problems that // mean it doesn't know the livekit URL fast enough: remove this once this is fixed. setLivekitServiceURL(newURL) { this.livekitServiceURL = newURL; } /** * 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. * @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. */ setGuest(guest) { 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) { this.logger.debug("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 this.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 }; this.logger.debug("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) { this.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`); } this.logger.debug("Crypto: Starting up crypto store..."); await this.cryptoStore.startup(); 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.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]); this.logger.debug("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. this.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 * * @param useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'. * * @returns a Promise which will resolve when the crypto layer has been * successfully initialised. */ async initRustCrypto({ useIndexedDB = true } = {}) { var _this$pickleKey; if (this.cryptoBackend) { this.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. this.logger.debug("Downloading Rust crypto library"); const RustCrypto = await Promise.resolve().then(() => _interopRequireWildcard(require("./rust-crypto"))); const rustCrypto = await RustCrypto.initRustCrypto({ logger: this.logger, http: this.http, userId: userId, deviceId: deviceId, secretStorage: this.secretStorage, cryptoCallbacks: this.cryptoCallbacks, storePrefix: useIndexedDB ? _constants.RUST_SDK_STORE_PREFIX : null, storePassphrase: this.pickleKey, legacyCryptoStore: this.cryptoStore, legacyPickleKey: (_this$pickleKey = this.pickleKey) !== null && _this$pickleKey !== void 0 ? _this$pickleKey : "DEFAULT_KEY", legacyMigrationProgressListener: (progress, total) => { this.emit(_crypto.CryptoEvent.LegacyCryptoStoreMigrationProgress, progress, total); } }); rustCrypto.setSupportedVerificationMethods(this.verificationMethods); this.cryptoBackend = rustCrypto; // attach the event listeners needed by RustCrypto this.on(_roomMember.RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto)); this.on(ClientEvent.Event, event => { rustCrypto.onLiveEventFromSync(event); }); // re-emit the events emitted by the crypto impl this.reEmitter.reEmit(rustCrypto, [_crypto.CryptoEvent.VerificationRequestReceived, _crypto.CryptoEvent.UserTrustStatusChanged, _crypto.CryptoEvent.KeyBackupStatus, _crypto.CryptoEvent.KeyBackupSessionsRemaining, _crypto.CryptoEvent.KeyBackupFailed, _crypto.CryptoEvent.KeyBackupDecryptionKeyCached, _crypto.CryptoEvent.KeysChanged, _crypto.CryptoEvent.DevicesUpdated, _crypto.CryptoEvent.WillUpdateDevices]); } /** * Access the server-side secret storage API for this client. */ get secretStorage() { return this._secretStorage; } /** * Access the crypto API for this client. * * If end-to-end encryption has been enabled for this client (via {@link initCrypto} or {@link initRustCrypto}), * returns an object giving access to the crypto API. Otherwise, returns `undefined`. */ getCrypto() { return this.cryptoBackend; } /** * Is end-to-end crypto enabled for this client. * @returns True if end-to-end is enabled. * @deprecated prefer {@link getCrypto} */ isCryptoEnabled() { return !!this.cryptoBackend; } /** * Get the Ed25519 key for this device * * @returns base64-encoded ed25519 key. Null if crypto is * disabled. * * @deprecated Prefer {@link CryptoApi.getOwnDeviceKeys} */ 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. * * @deprecated Use {@link CryptoApi.getOwnDeviceKeys} */ 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() { this.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-\>`DeviceInfo` * * @deprecated Prefer {@link CryptoApi.getUserDeviceInfo} */ 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 * @deprecated Prefer {@link CryptoApi.getUserDeviceInfo} */ 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 * @deprecated Prefer {@link CryptoApi.getUserDeviceInfo} */ 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