UNPKG

matrix-js-sdk

Version:
1,438 lines (1,142 loc) 289 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.RoomVersionStability = exports.PendingEventOrdering = exports.MatrixClient = exports.ClientEvent = exports.CRYPTO_ENABLED = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _matrixEventsSdk = require("matrix-events-sdk"); 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 _matrix = require("./matrix"); var _api = require("./crypto/api"); var ContentHelpers = _interopRequireWildcard(require("./content-helpers")); 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 _mediaHandler = require("./webrtc/mediaHandler"); var _typedEventEmitter = require("./models/typed-event-emitter"); var _read_receipts = require("./@types/read_receipts"); var _thread = require("./models/thread"); var _beacon = require("./@types/beacon"); 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; } 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; } 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 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 = {})); /* 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 || (exports.ClientEvent = ClientEvent = {})); /** * 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. // 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. // 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 constructor(opts) { 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, "unstableClientRelationAggregation", false); (0, _defineProperty2.default)(this, "identityServer", void 0); (0, _defineProperty2.default)(this, "sessionStore", void 0); (0, _defineProperty2.default)(this, "http", void 0); (0, _defineProperty2.default)(this, "crypto", void 0); (0, _defineProperty2.default)(this, "cryptoCallbacks", void 0); (0, _defineProperty2.default)(this, "callEventHandler", 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, "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, "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, "pendingEventEncryption", new Map()); (0, _defineProperty2.default)(this, "startCallEventHandler", () => { if (this.isInitialSyncComplete()) { this.callEventHandler.start(); this.off(ClientEvent.Sync, this.startCallEventHandler); } }); opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl); opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl); this.baseUrl = opts.baseUrl; this.idBaseUrl = opts.idBaseUrl; this.usingExternalCrypto = opts.usingExternalCrypto; this.store = opts.store || new _stub.StubStore(); this.deviceId = opts.deviceId || null; const userId = opts.userId || null; this.credentials = { userId }; this.http = new _httpApi.MatrixHttpApi(this, { baseUrl: opts.baseUrl, idBaseUrl: opts.idBaseUrl, accessToken: opts.accessToken, request: opts.request, prefix: _httpApi.PREFIX_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.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.timelineSupport = Boolean(opts.timelineSupport); this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation; this.cryptoStore = opts.cryptoStore; this.sessionStore = opts.sessionStore; 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; // 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); // 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 => { var _oldActions$tweaks, _actions$tweaks; const oldActions = event.getPushActions(); const actions = this.getPushActionsForEvent(event, true); const room = this.getRoom(event.getRoomId()); if (!room) return; const currentCount = room.getUnreadNotificationCount(_matrix.NotificationCountType.Highlight); // Ensure the unread counts are kept up to date if the event is encrypted // We also want to make sure that the notification count goes up if we already // have encrypted events to avoid other code from resetting 'highlight' to zero. const oldHighlight = !!(oldActions !== null && oldActions !== void 0 && (_oldActions$tweaks = oldActions.tweaks) !== null && _oldActions$tweaks !== void 0 && _oldActions$tweaks.highlight); const newHighlight = !!(actions !== null && actions !== void 0 && (_actions$tweaks = actions.tweaks) !== null && _actions$tweaks !== void 0 && _actions$tweaks.highlight); if (oldHighlight !== newHighlight || currentCount > 0) { // TODO: Handle mentions received while the client is offline // See also https://github.com/vector-im/element-web/issues/9069 if (!room.hasUserReadEvent(this.getUserId(), event.getId())) { let newCount = currentCount; if (newHighlight && !oldHighlight) newCount++; if (!newHighlight && oldHighlight) newCount--; room.setUnreadNotificationCount(_matrix.NotificationCountType.Highlight, newCount); // Fix 'Mentions Only' rooms from not having the right badge count const totalCount = room.getUnreadNotificationCount(_matrix.NotificationCountType.Total); if (totalCount < newCount) { room.setUnreadNotificationCount(_matrix.NotificationCountType.Total, newCount); } } } }); // 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(_matrix.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 => { const read = content[eid][_read_receipts.ReceiptType.Read]; if (read && Object.keys(read).includes(this.getUserId())) return true; const readPrivate = content[eid][_read_receipts.ReceiptType.ReadPrivate]; if (readPrivate && Object.keys(readPrivate).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--) { 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.tweaks && pushActions.tweaks.highlight ? 1 : 0; } // Note: we don't need to handle 'total' notifications because the counts // will come from the server. room.setUnreadNotificationCount(_matrix.NotificationCountType.Highlight, highlightCount); } }); } /** * High level helper method to begin syncing and poll for new events. To listen for these * events, add a listener for {@link module:client~MatrixClient#event:"event"} * via {@link module:client~MatrixClient#on}. Alternatively, listen for specific * state change events. * @param {Object=} opts Options to apply when syncing. */ async startClient(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)); } if (this.crypto) { this.crypto.uploadDeviceKeys(); this.crypto.start(); } // 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 { const { serverSupport, stable } = await this.doesServerSupportThread(); _thread.Thread.setServerSideSupport(serverSupport, stable); } catch (e) { // Most likely cause is that `doesServerSupportThread` returned `null` (as it // is allowed to do) and thus we enter "degraded mode" on threads. _thread.Thread.setServerSideSupport(false, true); } // shallow-copy the opts dict before modifying and storing it this.clientOpts = Object.assign({}, opts); this.clientOpts.crypto = this.crypto; this.clientOpts.canResetEntireTimeline = roomId => { if (!this.canResetTimelineCallback) { return false; } return this.canResetTimelineCallback(roomId); }; this.syncApi = new _sync.SyncApi(this, this.clientOpts); this.syncApi.sync(); if (this.clientOpts.clientWellKnownPollPeriod !== undefined) { this.clientWellKnownIntervalID = setInterval(() => { this.fetchClientWellKnown(); }, 1000 * this.clientOpts.clientWellKnownPollPeriod); this.fetchClientWellKnown(); } } /** * High level helper method to stop the client from polling and allow a * clean shutdown. */ stopClient() { var _this$crypto, _this$syncApi, _this$peekSync, _this$callEventHandle; (_this$crypto = this.crypto) === null || _this$crypto === void 0 ? void 0 : _this$crypto.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 = null; (_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.callEventHandler = null; global.clearInterval(this.checkTurnServersIntervalID); if (this.clientWellKnownIntervalID !== undefined) { global.clearInterval(this.clientWellKnownIntervalID); } } /** * 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. * * @return {Promise<string>} Resolves to undefined if a device could not be dehydrated, or * to the new device ID if the dehydration was successful. * @return {module:http-api.MatrixError} 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(undefined, _httpApi.Method.Post, "/dehydrated_device/claim", undefined, { device_id: getDeviceResult.device_id }, { prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2" }); if (rehydrateResult.success === true) { 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 * @return {Promise} A promise of an object containing the dehydrated device */ async getDehydratedDevice() { try { return await this.http.authedRequest(undefined, _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.toString()); return; } } /** * Set the dehydration key. This will also periodically dehydrate devices to * the server. * * @param {Uint8Array} key the dehydration key * @param {IDehydratedDeviceKeyInfo} [keyInfo] Information about the key. Primarily for * information about how to generate the key from a passphrase. * @param {string} [deviceDisplayName] The device display name for the * dehydrated device. * @return {Promise} A promise that resolves when the dehydrated device is stored. */ 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 {Uint8Array} key the dehydration key * @param {IDehydratedDeviceKeyInfo} [keyInfo] Information about the key. Primarily for * information about how to generate the key from a passphrase. * @param {string} [deviceDisplayName] The device display name for the * dehydrated device. * @return {Promise<String>} 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} 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()); } return Promise.all(promises).then(); // .then to fix types } /** * Get the user-id of the logged-in user * * @return {?string} 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 domain for this client's MXID * @return {?string} 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". * @return {?string} 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 * @return {?string} device ID */ getDeviceId() { return this.deviceId; } /** * Check if the runtime environment supports VoIP calling. * @return {boolean} True if VoIP is supported. */ supportsVoip() { return this.canSupportVoip; } /** * @returns {MediaHandler} */ 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 {boolean} 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 {boolean} support True to advertise the 'm.call.transferee' capability */ setSupportsCallTransfer(support) { this.supportsCallTransfer = support; } /** * Creates a new call. * The place*Call methods on the returned call can be used to actually place a call * * @param {string} roomId The room the call is to be placed in. * @return {MatrixCall} the call or null if the browser doesn't support calling. */ createCall(roomId) { return (0, _call.createNewMatrixCall)(this, roomId); } /** * Get the current sync state. * @return {?SyncState} the sync state, which may be null. * @see module:client~MatrixClient#event:"sync" */ getSyncState() { if (!this.syncApi) { return null; } return this.syncApi.getSyncState(); } /** * 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. * @return {?Object} */ getSyncStateData() { if (!this.syncApi) { return null; } return this.syncApi.getSyncStateData(); } /** * Whether the initial sync has completed. * @return {boolean} 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. * @return {boolean} 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 {boolean} 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. * @return {?module:scheduler~MatrixScheduler} The scheduler or null */ 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. * @return {boolean} True if this resulted in a request being retried. */ retryImmediately() { return this.syncApi.retryImmediately(); } /** * Return the global notification EventTimelineSet, if any * * @return {EventTimelineSet} the globl notification EventTimelineSet */ getNotifTimelineSet() { return this.notifTimelineSet; } /** * Set the global notification EventTimelineSet * * @param {EventTimelineSet} set */ 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 {boolean} fresh True to ignore any cached values. * @return {Promise} Resolves to the capabilities of the homeserver * @return {module:http-api.MatrixError} 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(undefined, _httpApi.Method.Get, "/capabilities").catch(e => { // We swallow errors because we need a default object anyhow _logger.logger.error(e); }).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 * * 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.crypto) { _logger.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); return; } if (!this.sessionStore) { // this is temporary, the sessionstore is supposed to be going away throw new Error(`Cannot enable encryption: no sessionStore provided`); } 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, this.sessionStore, 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.crypto = crypto; } /** * Is end-to-end crypto enabled for this client. * @return {boolean} True if end-to-end is enabled. */ isCryptoEnabled() { return !!this.crypto; } /** * Get the Ed25519 key for this device * * @return {?string} base64-encoded ed25519 key. Null if crypto is * disabled. */ getDeviceEd25519Key() { if (!this.crypto) return null; return this.crypto.getDeviceEd25519Key(); } /** * Get the Curve25519 key for this device * * @return {?string} base64-encoded curve25519 key. Null if crypto is * disabled. */ getDeviceCurve25519Key() { if (!this.crypto) return null; return this.crypto.getDeviceCurve25519Key(); } /** * Upload the device keys to the homeserver. * @return {Promise<void>} A promise that will resolve when the keys are uploaded. */ async uploadKeys() { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } await this.crypto.uploadDeviceKeys(); } /** * Download the keys for a list of users and stores the keys in the session * store. * @param {Array} userIds The users to fetch. * @param {boolean} forceDownload Always download the keys even if cached. * * @return {Promise} A promise which resolves to a map userId->deviceId->{@link * module:crypto~DeviceInfo|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 {string} userId the user to list keys for. * * @return {module:crypto/deviceinfo[]} 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 {string} userId the user to list keys for. * @param {string} deviceId unique identifier for the device * * @return {module:crypto/deviceinfo} 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 {string} userId owner of the device * @param {string} deviceId unique identifier for the device or user's * cross-signing public key ID. * * @param {boolean=} verified whether to mark the device as verified. defaults * to 'true'. * * @returns {Promise} * * @fires module:client~event:MatrixClient"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 {string} userId owner of the device * @param {string} deviceId unique identifier for the device or user's * cross-signing public key ID. * * @param {boolean=} blocked whether to mark the device as blocked. defaults * to 'true'. * * @returns {Promise} * * @fires module:client~event:MatrixClient"deviceVerificationChanged" */ setDeviceBlocked(userId, deviceId, blocked = true) { return this.setDeviceVerification(userId, deviceId, null, blocked, null); } /** * Mark the given device as known/unknown * * @param {string} userId owner of the device * @param {string} deviceId unique identifier for the device or user's * cross-signing public key ID. * * @param {boolean=} known whether to mark the device as known. defaults * to 'true'. * * @returns {Promise} * * @fires module:client~event:MatrixClient"deviceVerificationChanged" */ setDeviceKnown(userId, deviceId, known = true) { return this.setDeviceVerification(userId, deviceId, null, null, known); } async setDeviceVerification(userId, deviceId, verified, blocked, known) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } await this.crypto.setDeviceVerification(userId, deviceId, verified, blocked, known); } /** * Request a key verification from another user, using a DM. * * @param {string} userId the user to request verification with * @param {string} roomId the room to use for verification * * @returns {Promise<module:crypto/verification/request/VerificationRequest>} resolves to a VerificationRequest * when the request has been sent to the other party. */ requestVerificationDM(userId, roomId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.requestVerificationDM(userId, roomId); } /** * Finds a DM verification request that is already in progress for the given room id * * @param {string} roomId the room to use for verification * * @returns {module:crypto/verification/request/VerificationRequest?} the VerificationRequest that is in progress, if any */ findVerificationRequestDMInProgress(roomId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.findVerificationRequestDMInProgress(roomId); } /** * Returns all to-device verification requests that are already in progress for the given user id * * @param {string} userId the ID of the user to query * * @returns {module:crypto/verification/request/VerificationRequest[]} the VerificationRequests that are in progress */ getVerificationRequestsToDeviceInProgress(userId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.getVerificationRequestsToDeviceInProgress(userId); } /** * Request a key verification from another user. * * @param {string} userId the user to request verification with * @param {Array} devices array of device IDs to send requests to. Defaults to * all devices owned by the user * * @returns {Promise<module:crypto/verification/request/VerificationRequest>} resolves to a VerificationRequest * when the request has been sent to the other party. */ requestVerification(userId, devices) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.requestVerification(userId, devices); } /** * Begin a key verification. * * @param {string} method the verification method to use * @param {string} userId the user to verify keys with * @param {string} deviceId the device to verify * * @returns {Verification} a verification object * @deprecated Use `requestVerification` instead. */ beginKeyVerification(method, userId, deviceId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.beginKeyVerification(method, userId, deviceId); } checkSecretStorageKey(key, info) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.checkSecretStorageKey(key, info); } /** * Set the global override for whether the client should ever send encrypted * messages to unverified devices. This provides the default for rooms which * do not specify a value. * * @param {boolean} value whether to blacklist all unverified devices by default */ setGlobalBlacklistUnverifiedDevices(value) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.setGlobalBlacklistUnverifiedDevices(value); } /** * @return {boolean} whether to blacklist all unverified devices by default */ getGlobalBlacklistUnverifiedDevices() { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.getGlobalBlacklistUnverifiedDevices(); } /** * Set whether sendMessage in a room with unknown and unverified devices * should throw an error and not send them message. This has 'Global' for * symmetry with setGlobalBlacklistUnverifiedDevices but there is currently * no room-level equivalent for this setting. * * This API is currently UNSTABLE and may change or be removed without notice. * * @param {boolean} value whether error on unknown devices */ setGlobalErrorOnUnknownDevices(value) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.setGlobalErrorOnUnknownDevices(value); } /** * @return {boolean} whether to error on unknown devices * * This API is currently UNSTABLE and may change or be removed without notice. */ getGlobalErrorOnUnknownDevices() { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.getGlobalErrorOnUnknownDevices(); } /** * Get the user's cross-signing key ID. * * The cross-signing API is currently UNSTABLE and may change without notice. * * @param {CrossSigningKey} [type=master] The type of key to get the ID of. One of * "master", "self_signing", or "user_signing". Defaults to "master". * * @returns {string} the key ID */ getCrossSigningId(type = _api.CrossSigningKey.Master) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.getCrossSigningId(type); } /** * Get the cross signing information for a given user. * * The cross-signing API is currently UNSTABLE and may change without notice. * * @param {string} userId the user ID to get the cross-signing info for. * * @returns {CrossSigningInfo} the cross signing information for the user. */ getStoredCrossSigningForUser(userId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.getStoredCrossSigningForUser(userId); } /** * Check whether a given user is trusted. * * The cross-signing API is currently UNSTABLE and may change without notice. * * @param {string} userId The ID of the user to check. * * @returns {UserTrustLevel} */ checkUserTrust(userId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.checkUserTrust(userId); } /** * Check whether a given device is trusted. * * The cross-signing API is currently UNSTABLE and may change without notice. * * @function module:client~MatrixClient#checkDeviceTrust * @param {string} userId The ID of the user whose devices is to be checked. * @param {string} deviceId The ID of the device to check * * @returns {DeviceTrustLevel} */ checkDeviceTrust(userId, deviceId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.checkDeviceTrust(userId, deviceId); } /** * Check whether one of our own devices is cross-signed by our * user's stored keys, regardless of whether we trust those keys yet. * * @param {string} deviceId The ID of the device to check * * @returns {boolean} true if the device is cross-signed */ checkIfOwnDeviceCrossSigned(deviceId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.checkIfOwnDeviceCrossSigned(deviceId); } /** * Check the copy of our cross-signing key that we have in the device list and * see if we can get the private key. If so, mark it as trusted. * @param {Object} opts ICheckOwnCrossSigningTrustOpts object */ checkOwnCrossSigningTrust(opts) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } return this.crypto.checkOwnCrossSigningTrust(opts); } /** * Checks that a given cross-signing private key matches a given public key. * This can be used by the getCrossSigningKey callback to verify that the * private key it is about to supply is the one that was requested. * @param {Uint8Array} privateKey The private key * @param {string} expectedPublicKey The public key * @returns {boolean} true if the key matches, otherwise false */ checkCrossSigningPrivateKey(privateKey, expectedPubli