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