matrix-js-sdk
Version:
Matrix Client-Server SDK for Javascript
1,438 lines (1,142 loc) • 289 kB
JavaScript
"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