"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.

  /**
   * The libolm crypto implementation, if it is in use.
   *
   * @deprecated This should not be used. Instead, use the methods exposed directly on this class or
   * (where they are available) via {@link getCrypto}.
   */
  // 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, "isVoipWithNoMediaAllowed", 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;
    this.isVoipWithNoMediaAllowed = opts.isVoipWithNoMediaAllowed || 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}`);
    }

    // Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a
    // no media WebRTC connection anyway.
    return new _groupCall.GroupCall(this, room, type, isPtt, intent, undefined, dataChannelsEnabled || this.isVoipWithNoMediaAllowed, dataChannelOptions, this.isVoipWithNoMediaAllowed).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));
  }

  /**
   * Access the crypto API for this client.
   *
   * If end-to-end encryption has been enabled for this client (via {@link initCrypto} or {@link initRustCrypto}),
   * returns an object giving access to the crypto API. Otherwise, returns `undefined`.
   */
  getCrypto() {
    return this.cryptoBackend;
  }

  /**
   * Is end-to-end crypto enabled for this client.
   * @returns True if end-to-end is enabled.
   * @deprecated prefer {@link getCrypto}
   */
  isCryptoEnabled() {
    return !!this.cryptoBackend;
  }

  /**
   * Get the Ed25519 key for this device
   *
   * @returns base64-encoded ed25519 key. Null if crypto is
   *    disabled.
   */
  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}
   */
  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 userId - the user to request verification with
   * @param roomId - the room to use for verification
   *
   * @returns 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 roomId - the room to use for verification
   *
   * @returns 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 userId - the ID of the user to query
   *
   * @returns 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 userId - the user to request verification with
   * @param devices - array of device IDs to send requests to.  Defaults to
   *    all devices owned by the user
   *
   * @returns 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 method - the verification method to use
   * @param userId - the user to verify keys with
   * @param deviceId - the device to verify
   *
   * @returns 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 value - whether to blacklist all unverified devices by default
   *
   * @deprecated Prefer direct access to {@link CryptoApi.globalBlacklistUnverifiedDevices}:
   *
   * ```javascript
   * client.getCrypto().globalBlacklistUnverifiedDevices = value;
   * ```
   */
  setGlobalBlacklistUnverifiedDevices(value) {
    if (!this.cryptoBackend) {
      throw new Error("End-to-end encryption disabled");
    }
    this.cryptoBackend.globalBlacklistUnverifiedDevices = value;
    return value;
  }

  /**
   * @returns whether to blacklist all unverified devices by default
   *
   * @deprecated Prefer direct access to {@link CryptoApi.globalBlacklistUnverifiedDevices}:
   *
   * ```javascript
   * value = client.getCrypto().globalBlacklistUnverifiedDevices;
   * ```
   */
  getGlobalBlacklistUnverifiedDevices() {
    if (!this.cryptoBackend) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.cryptoBackend.globalBlacklistUnverifiedDevices;
  }

  /**
   * 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 value - whether error on unknown devices
   *
   * @deprecated Prefer direct access to {@link CryptoApi.globalBlacklistUnverifiedDevices}:
   *
   * ```ts
   * client.getCrypto().globalBlacklistUnverifiedDevices = value;
   * ```
   */
  setGlobalErrorOnUnknownDevices(value) {
    if (!this.cryptoBackend) {
      throw new Error("End-to-end encryption disabled");
    }
    this.cryptoBackend.globalErrorOnUnknownDevices = value;
  }

  /**
   * @returns whether to error on unknown devices
   *
   * This API is currently UNSTABLE and may change or be removed without notice.
   */
  getGlobalErrorOnUnknownDevices() {
    if (!this.cryptoBackend) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.cryptoBackend.globalErrorOnUnknownDevices;
  }

  /**
   * Get the user's cross-signing key ID.
   *
   * The cross-signing API is currently UNSTABLE and may change without notice.
   *
   * @param type - The type of key to get the ID of.  One of
   *     "master", "self_signing", or "user_signing".  Defaults to "master".
   *
   * @returns 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 userId - the user ID to get the cross-signing info for.
   *
   * @returns 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 userId - The ID of the user to check.
   */
  checkUserTrust(userId) {
    if (!this.cryptoBackend) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.cryptoBackend.checkUserTrust(userId);
  }

  /**
   * Check whether a given device is trusted.
   *
   * The cross-signing API is currently UNSTABLE and may change without notice.
   *
   * @param userId - The ID of the user whose devices is to be checked.
   * @param deviceId - The ID of the device to check
   */
  checkDeviceTrust(userId, deviceId) {
    if (!this.cryptoBackend) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.cryptoBackend.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 deviceId - The ID of the device to check
   *
   * @returns 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 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 privateKey - The private key
   * @param expectedPublicKey - The public key
   * @returns true if the key matches, otherwise false
   */
  checkCrossSigningPrivateKey(privateKey, expectedPublicKey) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.checkCrossSigningPrivateKey(privateKey, expectedPublicKey);
  }

  // deprecated: use requestVerification instead
  legacyDeviceVerification(userId, deviceId, method) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.legacyDeviceVerification(userId, deviceId, method);
  }

  /**
   * Perform any background tasks that can be done before a message is ready to
   * send, in order to speed up sending of the message.
   * @param room - the room the event is in
   *
   * @deprecated Prefer {@link CryptoApi.prepareToEncrypt | `CryptoApi.prepareToEncrypt`}:
   *
   * ```javascript
   * client.getCrypto().prepareToEncrypt(room);
   * ```
   */
  prepareToEncrypt(room) {
    if (!this.cryptoBackend) {
      throw new Error("End-to-end encryption disabled");
    }
    this.cryptoBackend.prepareToEncrypt(room);
  }

  /**
   * Checks if the user has previously published cross-signing keys
   *
   * This means downloading the devicelist for the user and checking if the list includes
   * the cross-signing pseudo-device.
   *
   * @deprecated Prefer {@link CryptoApi.userHasCrossSigningKeys | `CryptoApi.userHasCrossSigningKeys`}:
   *
   * ```javascript
   * result = client.getCrypto().userHasCrossSigningKeys();
   * ```
   */
  userHasCrossSigningKeys() {
    if (!this.cryptoBackend) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.cryptoBackend.userHasCrossSigningKeys();
  }

  /**
   * Checks whether cross signing:
   * - is enabled on this account and trusted by this device
   * - has private keys either cached locally or stored in secret storage
   *
   * If this function returns false, bootstrapCrossSigning() can be used
   * to fix things such that it returns true. That is to say, after
   * bootstrapCrossSigning() completes successfully, this function should
   * return true.
   * @returns True if cross-signing is ready to be used on this device
   */
  isCrossSigningReady() {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.isCrossSigningReady();
  }

  /**
   * Bootstrap cross-signing by creating keys if needed. If everything is already
   * set up, then no changes are made, so this is safe to run to ensure
   * cross-signing is ready for use.
   *
   * This function:
   * - creates new cross-signing keys if they are not found locally cached nor in
   *   secret storage (if it has been setup)
   *
   * The cross-signing API is currently UNSTABLE and may change without notice.
   */
  bootstrapCrossSigning(opts) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.bootstrapCrossSigning(opts);
  }

  /**
   * Whether to trust a others users signatures of their devices.
   * If false, devices will only be considered 'verified' if we have
   * verified that device individually (effectively disabling cross-signing).
   *
   * Default: true
   *
   * @returns True if trusting cross-signed devices
   */
  getCryptoTrustCrossSignedDevices() {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.getCryptoTrustCrossSignedDevices();
  }

  /**
   * See getCryptoTrustCrossSignedDevices
   *
   * @param val - True to trust cross-signed devices
   */
  setCryptoTrustCrossSignedDevices(val) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    this.crypto.setCryptoTrustCrossSignedDevices(val);
  }

  /**
   * Counts the number of end to end session keys that are waiting to be backed up
   * @returns Promise which resolves to the number of sessions requiring backup
   */
  countSessionsNeedingBackup() {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.countSessionsNeedingBackup();
  }

  /**
   * Get information about the encryption of an event
   *
   * @param event - event to be checked
   * @returns The event information.
   */
  getEventEncryptionInfo(event) {
    if (!this.cryptoBackend) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.cryptoBackend.getEventEncryptionInfo(event);
  }

  /**
   * Create a recovery key from a user-supplied passphrase.
   *
   * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
   *
   * @param password - Passphrase string that can be entered by the user
   *     when restoring the backup as an alternative to entering the recovery key.
   *     Optional.
   * @returns Object with public key metadata, encoded private
   *     recovery key which should be disposed of after displaying to the user,
   *     and raw private key to avoid round tripping if needed.
   */
  createRecoveryKeyFromPassphrase(password) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.createRecoveryKeyFromPassphrase(password);
  }

  /**
   * Checks whether secret storage:
   * - is enabled on this account
   * - is storing cross-signing private keys
   * - is storing session backup key (if enabled)
   *
   * If this function returns false, bootstrapSecretStorage() can be used
   * to fix things such that it returns true. That is to say, after
   * bootstrapSecretStorage() completes successfully, this function should
   * return true.
   *
   * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
   *
   * @returns True if secret storage is ready to be used on this device
   */
  isSecretStorageReady() {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.isSecretStorageReady();
  }

  /**
   * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is
   * already set up, then no changes are made, so this is safe to run to ensure secret
   * storage is ready for use.
   *
   * This function
   * - creates a new Secure Secret Storage key if no default key exists
   *   - if a key backup exists, it is migrated to store the key in the Secret
   *     Storage
   * - creates a backup if none exists, and one is requested
   * - migrates Secure Secret Storage to use the latest algorithm, if an outdated
   *   algorithm is found
   *
   */
  bootstrapSecretStorage(opts) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.bootstrapSecretStorage(opts);
  }

  /**
   * Add a key for encrypting secrets.
   *
   * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
   *
   * @param algorithm - the algorithm used by the key
   * @param opts - the options for the algorithm.  The properties used
   *     depend on the algorithm given.
   * @param keyName - the name of the key.  If not given, a random name will be generated.
   *
   * @returns An object with:
   *     keyId: the ID of the key
   *     keyInfo: details about the key (iv, mac, passphrase)
   */
  addSecretStorageKey(algorithm, opts, keyName) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.addSecretStorageKey(algorithm, opts, keyName);
  }

  /**
   * Check whether we have a key with a given ID.
   *
   * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
   *
   * @param keyId - The ID of the key to check
   *     for. Defaults to the default key ID if not provided.
   * @returns Whether we have the key.
   */
  hasSecretStorageKey(keyId) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.hasSecretStorageKey(keyId);
  }

  /**
   * Store an encrypted secret on the server.
   *
   * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
   *
   * @param name - The name of the secret
   * @param secret - The secret contents.
   * @param keys - The IDs of the keys to use to encrypt the secret or null/undefined
   *     to use the default (will throw if no default key is set).
   */
  storeSecret(name, secret, keys) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.storeSecret(name, secret, keys);
  }

  /**
   * Get a secret from storage.
   *
   * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
   *
   * @param name - the name of the secret
   *
   * @returns the contents of the secret
   */
  getSecret(name) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.getSecret(name);
  }

  /**
   * Check if a secret is stored on the server.
   *
   * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
   *
   * @param name - the name of the secret
   * @returns map of key name to key info the secret is encrypted
   *     with, or null if it is not present or not encrypted with a trusted
   *     key
   */
  isSecretStored(name) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.isSecretStored(name);
  }

  /**
   * Request a secret from another device.
   *
   * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
   *
   * @param name - the name of the secret to request
   * @param devices - the devices to request the secret from
   *
   * @returns the secret request object
   */
  requestSecret(name, devices) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.requestSecret(name, devices);
  }

  /**
   * Get the current default key ID for encrypting secrets.
   *
   * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
   *
   * @returns The default key ID or null if no default key ID is set
   */
  getDefaultSecretStorageKeyId() {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.getDefaultSecretStorageKeyId();
  }

  /**
   * Set the current default key ID for encrypting secrets.
   *
   * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
   *
   * @param keyId - The new default key ID
   */
  setDefaultSecretStorageKeyId(keyId) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.setDefaultSecretStorageKeyId(keyId);
  }

  /**
   * Checks that a given secret storage private key matches a given public key.
   * This can be used by the getSecretStorageKey callback to verify that the
   * private key it is about to supply is the one that was requested.
   *
   * The Secure Secret Storage API is currently UNSTABLE and may change without notice.
   *
   * @param privateKey - The private key
   * @param expectedPublicKey - The public key
   * @returns true if the key matches, otherwise false
   */
  checkSecretStoragePrivateKey(privateKey, expectedPublicKey) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.checkSecretStoragePrivateKey(privateKey, expectedPublicKey);
  }

  /**
   * Get e2e information on the device that sent an event
   *
   * @param event - event to be checked
   */
  async getEventSenderDeviceInfo(event) {
    if (!this.crypto) {
      return null;
    }
    return this.crypto.getEventSenderDeviceInfo(event);
  }

  /**
   * Check if the sender of an event is verified
   *
   * @param event - event to be checked
   *
   * @returns true if the sender of this event has been verified using
   * {@link MatrixClient#setDeviceVerified}.
   */
  async isEventSenderVerified(event) {
    const device = await this.getEventSenderDeviceInfo(event);
    if (!device) {
      return false;
    }
    return device.isVerified();
  }

  /**
   * Get outgoing room key request for this event if there is one.
   * @param event - The event to check for
   *
   * @returns A room key request, or null if there is none
   */
  getOutgoingRoomKeyRequest(event) {
    if (!this.crypto) {
      throw new Error("End-to-End encryption disabled");
    }
    const wireContent = event.getWireContent();
    const requestBody = {
      session_id: wireContent.session_id,
      sender_key: wireContent.sender_key,
      algorithm: wireContent.algorithm,
      room_id: event.getRoomId()
    };
    if (!requestBody.session_id || !requestBody.sender_key || !requestBody.algorithm || !requestBody.room_id) {
      return Promise.resolve(null);
    }
    return this.crypto.cryptoStore.getOutgoingRoomKeyRequest(requestBody);
  }

  /**
   * Cancel a room key request for this event if one is ongoing and resend the
   * request.
   * @param event - event of which to cancel and resend the room
   *                            key request.
   * @returns A promise that will resolve when the key request is queued
   */
  cancelAndResendEventRoomKeyRequest(event) {
    if (!this.crypto) {
      throw new Error("End-to-End encryption disabled");
    }
    return event.cancelAndResendKeyRequest(this.crypto, this.getUserId());
  }

  /**
   * Enable end-to-end encryption for a room. This does not modify room state.
   * Any messages sent before the returned promise resolves will be sent unencrypted.
   * @param roomId - The room ID to enable encryption in.
   * @param config - The encryption config for the room.
   * @returns A promise that will resolve when encryption is set up.
   */
  setRoomEncryption(roomId, config) {
    if (!this.crypto) {
      throw new Error("End-to-End encryption disabled");
    }
    return this.crypto.setRoomEncryption(roomId, config);
  }

  /**
   * Whether encryption is enabled for a room.
   * @param roomId - the room id to query.
   * @returns whether encryption is enabled.
   */
  isRoomEncrypted(roomId) {
    const room = this.getRoom(roomId);
    if (!room) {
      // we don't know about this room, so can't determine if it should be
      // encrypted. Let's assume not.
      return false;
    }

    // if there is an 'm.room.encryption' event in this room, it should be
    // encrypted (independently of whether we actually support encryption)
    const ev = room.currentState.getStateEvents(_event2.EventType.RoomEncryption, "");
    if (ev) {
      return true;
    }

    // we don't have an m.room.encrypted event, but that might be because
    // the server is hiding it from us. Check the store to see if it was
    // previously encrypted.
    return this.roomList.isRoomEncrypted(roomId);
  }

  /**
   * Encrypts and sends a given object via Olm to-device messages to a given
   * set of devices.
   *
   * @param userDeviceMap - mapping from userId to deviceInfo
   *
   * @param payload - fields to include in the encrypted payload
   *
   * @returns Promise which
   *     resolves once the message has been encrypted and sent to the given
   *     userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }`
   *     of the successfully sent messages.
   */
  encryptAndSendToDevices(userDeviceInfoArr, payload) {
    if (!this.crypto) {
      throw new Error("End-to-End encryption disabled");
    }
    return this.crypto.encryptAndSendToDevices(userDeviceInfoArr, payload);
  }

  /**
   * Forces the current outbound group session to be discarded such
   * that another one will be created next time an event is sent.
   *
   * @param roomId - The ID of the room to discard the session for
   *
   * @deprecated Prefer {@link CryptoApi.forceDiscardSession | `CryptoApi.forceDiscardSession`}:
   *
   */
  forceDiscardSession(roomId) {
    if (!this.cryptoBackend) {
      throw new Error("End-to-End encryption disabled");
    }
    this.cryptoBackend.forceDiscardSession(roomId);
  }

  /**
   * Get a list containing all of the room keys
   *
   * This should be encrypted before returning it to the user.
   *
   * @returns a promise which resolves to a list of session export objects
   *
   * @deprecated Prefer {@link CryptoApi.exportRoomKeys | `CryptoApi.exportRoomKeys`}:
   *
   * ```javascript
   * sessionData = await client.getCrypto().exportRoomKeys();
   * ```
   */
  exportRoomKeys() {
    if (!this.cryptoBackend) {
      return Promise.reject(new Error("End-to-end encryption disabled"));
    }
    return this.cryptoBackend.exportRoomKeys();
  }

  /**
   * Import a list of room keys previously exported by exportRoomKeys
   *
   * @param keys - a list of session export objects
   *
   * @returns a promise which resolves when the keys have been imported
   */
  importRoomKeys(keys, opts) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.importRoomKeys(keys, opts);
  }

  /**
   * Force a re-check of the local key backup status against
   * what's on the server.
   *
   * @returns Object with backup info (as returned by
   *     getKeyBackupVersion) in backupInfo and
   *     trust information (as returned by isKeyBackupTrusted)
   *     in trustInfo.
   */
  checkKeyBackup() {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.backupManager.checkKeyBackup();
  }

  /**
   * Get information about the current key backup.
   * @returns Information object from API or null
   */
  async getKeyBackupVersion() {
    let res;
    try {
      res = await this.http.authedRequest(_httpApi.Method.Get, "/room_keys/version", undefined, undefined, {
        prefix: _httpApi.ClientPrefix.V3
      });
    } catch (e) {
      if (e.errcode === "M_NOT_FOUND") {
        return null;
      } else {
        throw e;
      }
    }
    _backup.BackupManager.checkBackupVersion(res);
    return res;
  }

  /**
   * @param info - key backup info dict from getKeyBackupVersion()
   */
  isKeyBackupTrusted(info) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.backupManager.isKeyBackupTrusted(info);
  }

  /**
   * @returns true if the client is configured to back up keys to
   *     the server, otherwise false. If we haven't completed a successful check
   *     of key backup status yet, returns null.
   */
  getKeyBackupEnabled() {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.backupManager.getKeyBackupEnabled();
  }

  /**
   * Enable backing up of keys, using data previously returned from
   * getKeyBackupVersion.
   *
   * @param info - Backup information object as returned by getKeyBackupVersion
   * @returns Promise which resolves when complete.
   */
  enableKeyBackup(info) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.backupManager.enableKeyBackup(info);
  }

  /**
   * Disable backing up of keys.
   */
  disableKeyBackup() {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    this.crypto.backupManager.disableKeyBackup();
  }

  /**
   * Set up the data required to create a new backup version.  The backup version
   * will not be created and enabled until createKeyBackupVersion is called.
   *
   * @param password - Passphrase string that can be entered by the user
   *     when restoring the backup as an alternative to entering the recovery key.
   *     Optional.
   *
   * @returns Object that can be passed to createKeyBackupVersion and
   *     additionally has a 'recovery_key' member with the user-facing recovery key string.
   */
  async prepareKeyBackupVersion(password, opts = {
    secureSecretStorage: false
  }) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }

    // eslint-disable-next-line camelcase
    const {
      algorithm,
      auth_data,
      recovery_key,
      privateKey
    } = await this.crypto.backupManager.prepareKeyBackupVersion(password);
    if (opts.secureSecretStorage) {
      await this.storeSecret("m.megolm_backup.v1", (0, olmlib.encodeBase64)(privateKey));
      _logger.logger.info("Key backup private key stored in secret storage");
    }
    return {
      algorithm,
      /* eslint-disable camelcase */
      auth_data,
      recovery_key
      /* eslint-enable camelcase */
    };
  }

  /**
   * Check whether the key backup private key is stored in secret storage.
   * @returns map of key name to key info the secret is
   *     encrypted with, or null if it is not present or not encrypted with a
   *     trusted key
   */
  isKeyBackupKeyStored() {
    return Promise.resolve(this.isSecretStored("m.megolm_backup.v1"));
  }

  /**
   * Create a new key backup version and enable it, using the information return
   * from prepareKeyBackupVersion.
   *
   * @param info - Info object from prepareKeyBackupVersion
   * @returns Object with 'version' param indicating the version created
   */
  async createKeyBackupVersion(info) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    await this.crypto.backupManager.createKeyBackupVersion(info);
    const data = {
      algorithm: info.algorithm,
      auth_data: info.auth_data
    };

    // Sign the backup auth data with the device key for backwards compat with
    // older devices with cross-signing. This can probably go away very soon in
    // favour of just signing with the cross-singing master key.
    // XXX: Private member access
    await this.crypto.signObject(data.auth_data);
    if (this.cryptoCallbacks.getCrossSigningKey &&
    // XXX: Private member access
    this.crypto.crossSigningInfo.getId()) {
      // now also sign the auth data with the cross-signing master key
      // we check for the callback explicitly here because we still want to be able
      // to create an un-cross-signed key backup if there is a cross-signing key but
      // no callback supplied.
      // XXX: Private member access
      await this.crypto.crossSigningInfo.signObject(data.auth_data, "master");
    }
    const res = await this.http.authedRequest(_httpApi.Method.Post, "/room_keys/version", undefined, data, {
      prefix: _httpApi.ClientPrefix.V3
    });

    // We could assume everything's okay and enable directly, but this ensures
    // we run the same signature verification that will be used for future
    // sessions.
    await this.checkKeyBackup();
    if (!this.getKeyBackupEnabled()) {
      _logger.logger.error("Key backup not usable even though we just created it");
    }
    return res;
  }
  async deleteKeyBackupVersion(version) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }

    // If we're currently backing up to this backup... stop.
    // (We start using it automatically in createKeyBackupVersion
    // so this is symmetrical).
    if (this.crypto.backupManager.version) {
      this.crypto.backupManager.disableKeyBackup();
    }
    const path = utils.encodeUri("/room_keys/version/$version", {
      $version: version
    });
    await this.http.authedRequest(_httpApi.Method.Delete, path, undefined, undefined, {
      prefix: _httpApi.ClientPrefix.V3
    });
  }
  makeKeyBackupPath(roomId, sessionId, version) {
    let path;
    if (sessionId !== undefined) {
      path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", {
        $roomId: roomId,
        $sessionId: sessionId
      });
    } else if (roomId !== undefined) {
      path = utils.encodeUri("/room_keys/keys/$roomId", {
        $roomId: roomId
      });
    } else {
      path = "/room_keys/keys";
    }
    const queryData = version === undefined ? undefined : {
      version
    };
    return {
      path,
      queryData
    };
  }

  /**
   * Back up session keys to the homeserver.
   * @param roomId - ID of the room that the keys are for Optional.
   * @param sessionId - ID of the session that the keys are for Optional.
   * @param version - backup version Optional.
   * @param data - Object keys to send
   * @returns a promise that will resolve when the keys
   * are uploaded
   */

  async sendKeyBackup(roomId, sessionId, version, data) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    const path = this.makeKeyBackupPath(roomId, sessionId, version);
    await this.http.authedRequest(_httpApi.Method.Put, path.path, path.queryData, data, {
      prefix: _httpApi.ClientPrefix.V3
    });
  }

  /**
   * Marks all group sessions as needing to be backed up and schedules them to
   * upload in the background as soon as possible.
   */
  async scheduleAllGroupSessionsForBackup() {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    await this.crypto.backupManager.scheduleAllGroupSessionsForBackup();
  }

  /**
   * Marks all group sessions as needing to be backed up without scheduling
   * them to upload in the background.
   * @returns Promise which resolves to the number of sessions requiring a backup.
   */
  flagAllGroupSessionsForBackup() {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    return this.crypto.backupManager.flagAllGroupSessionsForBackup();
  }
  isValidRecoveryKey(recoveryKey) {
    try {
      (0, _recoverykey.decodeRecoveryKey)(recoveryKey);
      return true;
    } catch (e) {
      return false;
    }
  }

  /**
   * Get the raw key for a key backup from the password
   * Used when migrating key backups into SSSS
   *
   * The cross-signing API is currently UNSTABLE and may change without notice.
   *
   * @param password - Passphrase
   * @param backupInfo - Backup metadata from `checkKeyBackup`
   * @returns key backup key
   */
  keyBackupKeyFromPassword(password, backupInfo) {
    return (0, _key_passphrase.keyFromAuthData)(backupInfo.auth_data, password);
  }

  /**
   * Get the raw key for a key backup from the recovery key
   * Used when migrating key backups into SSSS
   *
   * The cross-signing API is currently UNSTABLE and may change without notice.
   *
   * @param recoveryKey - The recovery key
   * @returns key backup key
   */
  keyBackupKeyFromRecoveryKey(recoveryKey) {
    return (0, _recoverykey.decodeRecoveryKey)(recoveryKey);
  }

  /**
   * Restore from an existing key backup via a passphrase.
   *
   * @param password - Passphrase
   * @param targetRoomId - Room ID to target a specific room.
   * Restores all rooms if omitted.
   * @param targetSessionId - Session ID to target a specific session.
   * Restores all sessions if omitted.
   * @param backupInfo - Backup metadata from `checkKeyBackup`
   * @param opts - Optional params such as callbacks
   * @returns Status of restoration with `total` and `imported`
   * key counts.
   */

  async restoreKeyBackupWithPassword(password, targetRoomId, targetSessionId, backupInfo, opts) {
    const privKey = await (0, _key_passphrase.keyFromAuthData)(backupInfo.auth_data, password);
    return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts);
  }

  /**
   * Restore from an existing key backup via a private key stored in secret
   * storage.
   *
   * @param backupInfo - Backup metadata from `checkKeyBackup`
   * @param targetRoomId - Room ID to target a specific room.
   * Restores all rooms if omitted.
   * @param targetSessionId - Session ID to target a specific session.
   * Restores all sessions if omitted.
   * @param opts - Optional params such as callbacks
   * @returns Status of restoration with `total` and `imported`
   * key counts.
   */
  async restoreKeyBackupWithSecretStorage(backupInfo, targetRoomId, targetSessionId, opts) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    const storedKey = await this.getSecret("m.megolm_backup.v1");

    // ensure that the key is in the right format.  If not, fix the key and
    // store the fixed version
    const fixedKey = (0, _crypto.fixBackupKey)(storedKey);
    if (fixedKey) {
      const keys = await this.crypto.getSecretStorageKey();
      await this.storeSecret("m.megolm_backup.v1", fixedKey, [keys[0]]);
    }
    const privKey = (0, olmlib.decodeBase64)(fixedKey || storedKey);
    return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts);
  }

  /**
   * Restore from an existing key backup via an encoded recovery key.
   *
   * @param recoveryKey - Encoded recovery key
   * @param targetRoomId - Room ID to target a specific room.
   * Restores all rooms if omitted.
   * @param targetSessionId - Session ID to target a specific session.
   * Restores all sessions if omitted.
   * @param backupInfo - Backup metadata from `checkKeyBackup`
   * @param opts - Optional params such as callbacks
    * @returns Status of restoration with `total` and `imported`
   * key counts.
   */

  restoreKeyBackupWithRecoveryKey(recoveryKey, targetRoomId, targetSessionId, backupInfo, opts) {
    const privKey = (0, _recoverykey.decodeRecoveryKey)(recoveryKey);
    return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts);
  }
  async restoreKeyBackupWithCache(targetRoomId, targetSessionId, backupInfo, opts) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    const privKey = await this.crypto.getSessionBackupPrivateKey();
    if (!privKey) {
      throw new Error("Couldn't get key");
    }
    return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts);
  }
  async restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts) {
    const cacheCompleteCallback = opts === null || opts === void 0 ? void 0 : opts.cacheCompleteCallback;
    const progressCallback = opts === null || opts === void 0 ? void 0 : opts.progressCallback;
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    let totalKeyCount = 0;
    let keys = [];
    const path = this.makeKeyBackupPath(targetRoomId, targetSessionId, backupInfo.version);
    const algorithm = await _backup.BackupManager.makeAlgorithm(backupInfo, async () => {
      return privKey;
    });
    const untrusted = algorithm.untrusted;
    try {
      // If the pubkey computed from the private data we've been given
      // doesn't match the one in the auth_data, the user has entered
      // a different recovery key / the wrong passphrase.
      if (!(await algorithm.keyMatches(privKey))) {
        return Promise.reject(new _httpApi.MatrixError({
          errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY
        }));
      }

      // Cache the key, if possible.
      // This is async.
      this.crypto.storeSessionBackupPrivateKey(privKey).catch(e => {
        _logger.logger.warn("Error caching session backup key:", e);
      }).then(cacheCompleteCallback);
      if (progressCallback) {
        progressCallback({
          stage: "fetch"
        });
      }
      const res = await this.http.authedRequest(_httpApi.Method.Get, path.path, path.queryData, undefined, {
        prefix: _httpApi.ClientPrefix.V3
      });
      if (res.rooms) {
        const rooms = res.rooms;
        for (const [roomId, roomData] of Object.entries(rooms)) {
          if (!roomData.sessions) continue;
          totalKeyCount += Object.keys(roomData.sessions).length;
          const roomKeys = await algorithm.decryptSessions(roomData.sessions);
          for (const k of roomKeys) {
            k.room_id = roomId;
            keys.push(k);
          }
        }
      } else if (res.sessions) {
        const sessions = res.sessions;
        totalKeyCount = Object.keys(sessions).length;
        keys = await algorithm.decryptSessions(sessions);
        for (const k of keys) {
          k.room_id = targetRoomId;
        }
      } else {
        totalKeyCount = 1;
        try {
          const [key] = await algorithm.decryptSessions({
            [targetSessionId]: res
          });
          key.room_id = targetRoomId;
          key.session_id = targetSessionId;
          keys.push(key);
        } catch (e) {
          _logger.logger.log("Failed to decrypt megolm session from backup", e);
        }
      }
    } finally {
      algorithm.free();
    }
    await this.importRoomKeys(keys, {
      progressCallback,
      untrusted,
      source: "backup"
    });
    await this.checkKeyBackup();
    return {
      total: totalKeyCount,
      imported: keys.length
    };
  }
  async deleteKeysFromBackup(roomId, sessionId, version) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    const path = this.makeKeyBackupPath(roomId, sessionId, version);
    await this.http.authedRequest(_httpApi.Method.Delete, path.path, path.queryData, undefined, {
      prefix: _httpApi.ClientPrefix.V3
    });
  }

  /**
   * Share shared-history decryption keys with the given users.
   *
   * @param roomId - the room for which keys should be shared.
   * @param userIds - a list of users to share with.  The keys will be sent to
   *     all of the user's current devices.
   */
  async sendSharedHistoryKeys(roomId, userIds) {
    if (!this.crypto) {
      throw new Error("End-to-end encryption disabled");
    }
    const roomEncryption = this.roomList.getRoomEncryption(roomId);
    if (!roomEncryption) {
      // unknown room, or unencrypted room
      _logger.logger.error("Unknown room.  Not sharing decryption keys");
      return;
    }
    const deviceInfos = await this.crypto.downloadKeys(userIds);
    const devicesByUser = new Map();
    for (const [userId, devices] of deviceInfos) {
      devicesByUser.set(userId, Array.from(devices.values()));
    }

    // XXX: Private member access
    const alg = this.crypto.getRoomDecryptor(roomId, roomEncryption.algorithm);
    if (alg.sendSharedHistoryInboundSessions) {
      await alg.sendSharedHistoryInboundSessions(devicesByUser);
    } else {
      _logger.logger.warn("Algorithm does not support sharing previous keys", roomEncryption.algorithm);
    }
  }

  /**
   * Get the config for the media repository.
   * @returns Promise which resolves with an object containing the config.
   */
  getMediaConfig() {
    return this.http.authedRequest(_httpApi.Method.Get, "/config", undefined, undefined, {
      prefix: _httpApi.MediaPrefix.R0
    });
  }

  /**
   * Get the room for the given room ID.
   * This function will return a valid room for any room for which a Room event
   * has been emitted. Note in particular that other events, eg. RoomState.members
   * will be emitted for a room before this function will return the given room.
   * @param roomId - The room ID
   * @returns The Room or null if it doesn't exist or there is no data store.
   */
  getRoom(roomId) {
    if (!roomId) {
      return null;
    }
    return this.store.getRoom(roomId);
  }

  /**
   * Retrieve all known rooms.
   * @returns A list of rooms, or an empty list if there is no data store.
   */
  getRooms() {
    return this.store.getRooms();
  }

  /**
   * Retrieve all rooms that should be displayed to the user
   * This is essentially getRooms() with some rooms filtered out, eg. old versions
   * of rooms that have been replaced or (in future) other rooms that have been
   * marked at the protocol level as not to be displayed to the user.
   *
   * @param msc3946ProcessDynamicPredecessor - if true, look for an
   *                                           m.room.predecessor state event and
   *                                           use it if found (MSC3946).
   * @returns A list of rooms, or an empty list if there is no data store.
   */
  getVisibleRooms(msc3946ProcessDynamicPredecessor = false) {
    const allRooms = this.store.getRooms();
    const replacedRooms = new Set();
    for (const r of allRooms) {
      var _r$findPredecessor;
      const predecessor = (_r$findPredecessor = r.findPredecessor(msc3946ProcessDynamicPredecessor)) === null || _r$findPredecessor === void 0 ? void 0 : _r$findPredecessor.roomId;
      if (predecessor) {
        replacedRooms.add(predecessor);
      }
    }
    return allRooms.filter(r => {
      const tombstone = r.currentState.getStateEvents(_event2.EventType.RoomTombstone, "");
      if (tombstone && replacedRooms.has(r.roomId)) {
        return false;
      }
      return true;
    });
  }

  /**
   * Retrieve a user.
   * @param userId - The user ID to retrieve.
   * @returns A user or null if there is no data store or the user does
   * not exist.
   */
  getUser(userId) {
    return this.store.getUser(userId);
  }

  /**
   * Retrieve all known users.
   * @returns A list of users, or an empty list if there is no data store.
   */
  getUsers() {
    return this.store.getUsers();
  }

  /**
   * Set account data event for the current user.
   * It will retry the request up to 5 times.
   * @param eventType - The event type
   * @param content - the contents object for the event
   * @returns Promise which resolves: an empty object
   * @returns Rejects: with an error response.
   */
  setAccountData(eventType, content) {
    const path = utils.encodeUri("/user/$userId/account_data/$type", {
      $userId: this.credentials.userId,
      $type: eventType
    });
    return (0, _httpApi.retryNetworkOperation)(5, () => {
      return this.http.authedRequest(_httpApi.Method.Put, path, undefined, content);
    });
  }

  /**
   * Get account data event of given type for the current user.
   * @param eventType - The event type
   * @returns The contents of the given account data event
   */
  getAccountData(eventType) {
    return this.store.getAccountData(eventType);
  }

  /**
   * Get account data event of given type for the current user. This variant
   * gets account data directly from the homeserver if the local store is not
   * ready, which can be useful very early in startup before the initial sync.
   * @param eventType - The event type
   * @returns Promise which resolves: The contents of the given account data event.
   * @returns Rejects: with an error response.
   */
  async getAccountDataFromServer(eventType) {
    if (this.isInitialSyncComplete()) {
      const event = this.store.getAccountData(eventType);
      if (!event) {
        return null;
      }
      // The network version below returns just the content, so this branch
      // does the same to match.
      return event.getContent();
    }
    const path = utils.encodeUri("/user/$userId/account_data/$type", {
      $userId: this.credentials.userId,
      $type: eventType
    });
    try {
      return await this.http.authedRequest(_httpApi.Method.Get, path);
    } catch (e) {
      var _data;
      if (((_data = e.data) === null || _data === void 0 ? void 0 : _data.errcode) === "M_NOT_FOUND") {
        return null;
      }
      throw e;
    }
  }
  async deleteAccountData(eventType) {
    const msc3391DeleteAccountDataServerSupport = this.canSupport.get(_feature.Feature.AccountDataDeletion);
    // if deletion is not supported overwrite with empty content
    if (msc3391DeleteAccountDataServerSupport === _feature.ServerSupport.Unsupported) {
      await this.setAccountData(eventType, {});
      return;
    }
    const path = utils.encodeUri("/user/$userId/account_data/$type", {
      $userId: this.getSafeUserId(),
      $type: eventType
    });
    const options = msc3391DeleteAccountDataServerSupport === _feature.ServerSupport.Unstable ? {
      prefix: "/_matrix/client/unstable/org.matrix.msc3391"
    } : undefined;
    return await this.http.authedRequest(_httpApi.Method.Delete, path, undefined, undefined, options);
  }

  /**
   * Gets the users that are ignored by this client
   * @returns The array of users that are ignored (empty if none)
   */
  getIgnoredUsers() {
    const event = this.getAccountData("m.ignored_user_list");
    if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return [];
    return Object.keys(event.getContent()["ignored_users"]);
  }

  /**
   * Sets the users that the current user should ignore.
   * @param userIds - the user IDs to ignore
   * @returns Promise which resolves: an empty object
   * @returns Rejects: with an error response.
   */
  setIgnoredUsers(userIds) {
    const content = {
      ignored_users: {}
    };
    userIds.forEach(u => {
      content.ignored_users[u] = {};
    });
    return this.setAccountData("m.ignored_user_list", content);
  }

  /**
   * Gets whether or not a specific user is being ignored by this client.
   * @param userId - the user ID to check
   * @returns true if the user is ignored, false otherwise
   */
  isUserIgnored(userId) {
    return this.getIgnoredUsers().includes(userId);
  }

  /**
   * Join a room. If you have already joined the room, this will no-op.
   * @param roomIdOrAlias - The room ID or room alias to join.
   * @param opts - Options when joining the room.
   * @returns Promise which resolves: Room object.
   * @returns Rejects: with an error response.
   */
  async joinRoom(roomIdOrAlias, opts = {}) {
    if (opts.syncRoom === undefined) {
      opts.syncRoom = true;
    }
    const room = this.getRoom(roomIdOrAlias);
    if (room !== null && room !== void 0 && room.hasMembershipState(this.credentials.userId, "join")) {
      return Promise.resolve(room);
    }
    let signPromise = Promise.resolve();
    if (opts.inviteSignUrl) {
      const url = new URL(opts.inviteSignUrl);
      url.searchParams.set("mxid", this.credentials.userId);
      signPromise = this.http.requestOtherUrl(_httpApi.Method.Post, url);
    }
    const queryString = {};
    if (opts.viaServers) {
      queryString["server_name"] = opts.viaServers;
    }
    try {
      const data = {};
      const signedInviteObj = await signPromise;
      if (signedInviteObj) {
        data.third_party_signed = signedInviteObj;
      }
      const path = utils.encodeUri("/join/$roomid", {
        $roomid: roomIdOrAlias
      });
      const res = await this.http.authedRequest(_httpApi.Method.Post, path, queryString, data);
      const roomId = res.room_id;
      const syncApi = new _sync.SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
      const room = syncApi.createRoom(roomId);
      if (opts.syncRoom) {
        // v2 will do this for us
        // return syncApi.syncRoom(room);
      }
      return room;
    } catch (e) {
      throw e; // rethrow for reject
    }
  }

  /**
   * Resend an event. Will also retry any to-device messages waiting to be sent.
   * @param event - The event to resend.
   * @param room - Optional. The room the event is in. Will update the
   * timeline entry if provided.
   * @returns Promise which resolves: to an ISendEventResponse object
   * @returns Rejects: with an error response.
   */
  resendEvent(event, room) {
    // also kick the to-device queue to retry
    this.toDeviceMessageQueue.sendQueue();
    this.updatePendingEventStatus(room, event, _event.EventStatus.SENDING);
    return this.encryptAndSendEvent(room, event);
  }

  /**
   * Cancel a queued or unsent event.
   *
   * @param event -   Event to cancel
   * @throws Error if the event is not in QUEUED, NOT_SENT or ENCRYPTING state
   */
  cancelPendingEvent(event) {
    if (![_event.EventStatus.QUEUED, _event.EventStatus.NOT_SENT, _event.EventStatus.ENCRYPTING].includes(event.status)) {
      throw new Error("cannot cancel an event with status " + event.status);
    }

    // if the event is currently being encrypted then
    if (event.status === _event.EventStatus.ENCRYPTING) {
      this.pendingEventEncryption.delete(event.getId());
    } else if (this.scheduler && event.status === _event.EventStatus.QUEUED) {
      // tell the scheduler to forget about it, if it's queued
      this.scheduler.removeEventFromQueue(event);
    }

    // then tell the room about the change of state, which will remove it
    // from the room's list of pending events.
    const room = this.getRoom(event.getRoomId());
    this.updatePendingEventStatus(room, event, _event.EventStatus.CANCELLED);
  }

  /**
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  setRoomName(roomId, name) {
    return this.sendStateEvent(roomId, _event2.EventType.RoomName, {
      name: name
    });
  }

  /**
   * @param htmlTopic - Optional.
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  setRoomTopic(roomId, topic, htmlTopic) {
    const content = ContentHelpers.makeTopicContent(topic, htmlTopic);
    return this.sendStateEvent(roomId, _event2.EventType.RoomTopic, content);
  }

  /**
   * @returns Promise which resolves: to an object keyed by tagId with objects containing a numeric order field.
   * @returns Rejects: with an error response.
   */
  getRoomTags(roomId) {
    const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags", {
      $userId: this.credentials.userId,
      $roomId: roomId
    });
    return this.http.authedRequest(_httpApi.Method.Get, path);
  }

  /**
   * @param tagName - name of room tag to be set
   * @param metadata - associated with that tag to be stored
   * @returns Promise which resolves: to an empty object
   * @returns Rejects: with an error response.
   */
  setRoomTag(roomId, tagName, metadata) {
    const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
      $userId: this.credentials.userId,
      $roomId: roomId,
      $tag: tagName
    });
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, metadata);
  }

  /**
   * @param tagName - name of room tag to be removed
   * @returns Promise which resolves: to an empty object
   * @returns Rejects: with an error response.
   */
  deleteRoomTag(roomId, tagName) {
    const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
      $userId: this.credentials.userId,
      $roomId: roomId,
      $tag: tagName
    });
    return this.http.authedRequest(_httpApi.Method.Delete, path);
  }

  /**
   * @param eventType - event type to be set
   * @param content - event content
   * @returns Promise which resolves: to an empty object `{}`
   * @returns Rejects: with an error response.
   */
  setRoomAccountData(roomId, eventType, content) {
    const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", {
      $userId: this.credentials.userId,
      $roomId: roomId,
      $type: eventType
    });
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, content);
  }

  /**
   * Set a power level to one or multiple users.
   * @returns Promise which resolves: to an ISendEventResponse object
   * @returns Rejects: with an error response.
   */
  setPowerLevel(roomId, userId, powerLevel, event) {
    let content = {
      users: {}
    };
    if ((event === null || event === void 0 ? void 0 : event.getType()) === _event2.EventType.RoomPowerLevels) {
      // take a copy of the content to ensure we don't corrupt
      // existing client state with a failed power level change
      content = utils.deepCopy(event.getContent());
    }
    const users = Array.isArray(userId) ? userId : [userId];
    for (const user of users) {
      if (powerLevel == null) {
        delete content.users[user];
      } else {
        content.users[user] = powerLevel;
      }
    }
    const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", {
      $roomId: roomId
    });
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, content);
  }

  /**
   * Create an m.beacon_info event
   * @returns
   */
  // eslint-disable-next-line @typescript-eslint/naming-convention
  async unstable_createLiveBeacon(roomId, beaconInfoContent) {
    return this.unstable_setLiveBeacon(roomId, beaconInfoContent);
  }

  /**
   * Upsert a live beacon event
   * using a specific m.beacon_info.* event variable type
   * @param roomId - string
   * @returns
   */
  // eslint-disable-next-line @typescript-eslint/naming-convention
  async unstable_setLiveBeacon(roomId, beaconInfoContent) {
    return this.sendStateEvent(roomId, _beacon.M_BEACON_INFO.name, beaconInfoContent, this.getUserId());
  }
  sendEvent(roomId, threadIdOrEventType, eventTypeOrContent, contentOrTxnId, txnIdOrVoid) {
    var _mRelates_to;
    let threadId;
    let eventType;
    let content;
    let txnId;
    if (!(threadIdOrEventType !== null && threadIdOrEventType !== void 0 && threadIdOrEventType.startsWith(EVENT_ID_PREFIX)) && threadIdOrEventType !== null) {
      txnId = contentOrTxnId;
      content = eventTypeOrContent;
      eventType = threadIdOrEventType;
      threadId = null;
    } else {
      txnId = txnIdOrVoid;
      content = contentOrTxnId;
      eventType = eventTypeOrContent;
      threadId = threadIdOrEventType;
    }

    // If we expect that an event is part of a thread but is missing the relation
    // we need to add it manually, as well as the reply fallback
    if (threadId && !((_mRelates_to = content["m.relates_to"]) !== null && _mRelates_to !== void 0 && _mRelates_to.rel_type)) {
      var _mRelates_to2, _this$getRoom;
      const isReply = !!((_mRelates_to2 = content["m.relates_to"]) !== null && _mRelates_to2 !== void 0 && _mRelates_to2["m.in_reply_to"]);
      content["m.relates_to"] = _objectSpread(_objectSpread({}, content["m.relates_to"]), {}, {
        rel_type: _thread.THREAD_RELATION_TYPE.name,
        event_id: threadId,
        // Set is_falling_back to true unless this is actually intended to be a reply
        is_falling_back: !isReply
      });
      const thread = (_this$getRoom = this.getRoom(roomId)) === null || _this$getRoom === void 0 ? void 0 : _this$getRoom.getThread(threadId);
      if (thread && !isReply) {
        var _thread$lastReply$get, _thread$lastReply;
        content["m.relates_to"]["m.in_reply_to"] = {
          event_id: (_thread$lastReply$get = (_thread$lastReply = thread.lastReply(ev => {
            return ev.isRelation(_thread.THREAD_RELATION_TYPE.name) && !ev.status;
          })) === null || _thread$lastReply === void 0 ? void 0 : _thread$lastReply.getId()) !== null && _thread$lastReply$get !== void 0 ? _thread$lastReply$get : threadId
        };
      }
    }
    return this.sendCompleteEvent(roomId, threadId, {
      type: eventType,
      content
    }, txnId);
  }

  /**
   * @param eventObject - An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added.
   * @param txnId - Optional.
   * @returns Promise which resolves: to an empty object `{}`
   * @returns Rejects: with an error response.
   */
  sendCompleteEvent(roomId, threadId, eventObject, txnId) {
    if (!txnId) {
      txnId = this.makeTxnId();
    }

    // We always construct a MatrixEvent when sending because the store and scheduler use them.
    // We'll extract the params back out if it turns out the client has no scheduler or store.
    const localEvent = new _event.MatrixEvent(Object.assign(eventObject, {
      event_id: "~" + roomId + ":" + txnId,
      user_id: this.credentials.userId,
      sender: this.credentials.userId,
      room_id: roomId,
      origin_server_ts: new Date().getTime()
    }));
    const room = this.getRoom(roomId);
    const thread = threadId ? room === null || room === void 0 ? void 0 : room.getThread(threadId) : undefined;
    if (thread) {
      localEvent.setThread(thread);
    }

    // set up re-emitter for this new event - this is normally the job of EventMapper but we don't use it here
    this.reEmitter.reEmit(localEvent, [_event.MatrixEventEvent.Replaced, _event.MatrixEventEvent.VisibilityChange]);
    room === null || room === void 0 ? void 0 : room.reEmitter.reEmit(localEvent, [_event.MatrixEventEvent.BeforeRedaction]);

    // if this is a relation or redaction of an event
    // that hasn't been sent yet (e.g. with a local id starting with a ~)
    // then listen for the remote echo of that event so that by the time
    // this event does get sent, we have the correct event_id
    const targetId = localEvent.getAssociatedId();
    if (targetId !== null && targetId !== void 0 && targetId.startsWith("~")) {
      const target = room === null || room === void 0 ? void 0 : room.getPendingEvents().find(e => e.getId() === targetId);
      target === null || target === void 0 ? void 0 : target.once(_event.MatrixEventEvent.LocalEventIdReplaced, () => {
        localEvent.updateAssociatedId(target.getId());
      });
    }
    const type = localEvent.getType();
    _logger.logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
    localEvent.setTxnId(txnId);
    localEvent.setStatus(_event.EventStatus.SENDING);

    // add this event immediately to the local store as 'sending'.
    room === null || room === void 0 ? void 0 : room.addPendingEvent(localEvent, txnId);

    // addPendingEvent can change the state to NOT_SENT if it believes
    // that there's other events that have failed. We won't bother to
    // try sending the event if the state has changed as such.
    if (localEvent.status === _event.EventStatus.NOT_SENT) {
      return Promise.reject(new Error("Event blocked by other events not yet sent"));
    }
    return this.encryptAndSendEvent(room, localEvent);
  }

  /**
   * encrypts the event if necessary; adds the event to the queue, or sends it; marks the event as sent/unsent
   * @returns returns a promise which resolves with the result of the send request
   */
  encryptAndSendEvent(room, event) {
    let cancelled = false;
    // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections,
    // so that we can handle synchronous and asynchronous exceptions with the
    // same code path.
    return Promise.resolve().then(() => {
      const encryptionPromise = this.encryptEventIfNeeded(event, room !== null && room !== void 0 ? room : undefined);
      if (!encryptionPromise) return null; // doesn't need encryption

      this.pendingEventEncryption.set(event.getId(), encryptionPromise);
      this.updatePendingEventStatus(room, event, _event.EventStatus.ENCRYPTING);
      return encryptionPromise.then(() => {
        if (!this.pendingEventEncryption.has(event.getId())) {
          // cancelled via MatrixClient::cancelPendingEvent
          cancelled = true;
          return;
        }
        this.updatePendingEventStatus(room, event, _event.EventStatus.SENDING);
      });
    }).then(() => {
      if (cancelled) return {};
      let promise = null;
      if (this.scheduler) {
        // if this returns a promise then the scheduler has control now and will
        // resolve/reject when it is done. Internally, the scheduler will invoke
        // processFn which is set to this._sendEventHttpRequest so the same code
        // path is executed regardless.
        promise = this.scheduler.queueEvent(event);
        if (promise && this.scheduler.getQueueForEvent(event).length > 1) {
          // event is processed FIFO so if the length is 2 or more we know
          // this event is stuck behind an earlier event.
          this.updatePendingEventStatus(room, event, _event.EventStatus.QUEUED);
        }
      }
      if (!promise) {
        promise = this.sendEventHttpRequest(event);
        if (room) {
          promise = promise.then(res => {
            room.updatePendingEvent(event, _event.EventStatus.SENT, res["event_id"]);
            return res;
          });
        }
      }
      return promise;
    }).catch(err => {
      _logger.logger.error("Error sending event", err.stack || err);
      try {
        // set the error on the event before we update the status:
        // updating the status emits the event, so the state should be
        // consistent at that point.
        event.error = err;
        this.updatePendingEventStatus(room, event, _event.EventStatus.NOT_SENT);
      } catch (e) {
        _logger.logger.error("Exception in error handler!", e.stack || err);
      }
      if (err instanceof _httpApi.MatrixError) {
        err.event = event;
      }
      throw err;
    });
  }
  encryptEventIfNeeded(event, room) {
    if (event.isEncrypted()) {
      // this event has already been encrypted; this happens if the
      // encryption step succeeded, but the send step failed on the first
      // attempt.
      return null;
    }
    if (event.isRedaction()) {
      // Redactions do not support encryption in the spec at this time,
      // whilst it mostly worked in some clients, it wasn't compliant.
      return null;
    }
    if (!room || !this.isRoomEncrypted(event.getRoomId())) {
      return null;
    }
    if (!this.cryptoBackend && this.usingExternalCrypto) {
      // The client has opted to allow sending messages to encrypted
      // rooms even if the room is encrypted, and we haven't setup
      // crypto. This is useful for users of matrix-org/pantalaimon
      return null;
    }
    if (event.getType() === _event2.EventType.Reaction) {
      // For reactions, there is a very little gained by encrypting the entire
      // event, as relation data is already kept in the clear. Event
      // encryption for a reaction effectively only obscures the event type,
      // but the purpose is still obvious from the relation data, so nothing
      // is really gained. It also causes quite a few problems, such as:
      //   * triggers notifications via default push rules
      //   * prevents server-side bundling for reactions
      // The reaction key / content / emoji value does warrant encrypting, but
      // this will be handled separately by encrypting just this value.
      // See https://github.com/matrix-org/matrix-doc/pull/1849#pullrequestreview-248763642
      return null;
    }
    if (!this.cryptoBackend) {
      throw new Error("This room is configured to use encryption, but your client does not support encryption.");
    }
    return this.cryptoBackend.encryptEvent(event, room);
  }

  /**
   * Returns the eventType that should be used taking encryption into account
   * for a given eventType.
   * @param roomId - the room for the events `eventType` relates to
   * @param eventType - the event type
   * @returns the event type taking encryption into account
   */
  getEncryptedIfNeededEventType(roomId, eventType) {
    if (eventType === _event2.EventType.Reaction) return eventType;
    return this.isRoomEncrypted(roomId) ? _event2.EventType.RoomMessageEncrypted : eventType;
  }
  updatePendingEventStatus(room, event, newStatus) {
    if (room) {
      room.updatePendingEvent(event, newStatus);
    } else {
      event.setStatus(newStatus);
    }
  }
  sendEventHttpRequest(event) {
    let txnId = event.getTxnId();
    if (!txnId) {
      txnId = this.makeTxnId();
      event.setTxnId(txnId);
    }
    const pathParams = {
      $roomId: event.getRoomId(),
      $eventType: event.getWireType(),
      $stateKey: event.getStateKey(),
      $txnId: txnId
    };
    let path;
    if (event.isState()) {
      let pathTemplate = "/rooms/$roomId/state/$eventType";
      if (event.getStateKey() && event.getStateKey().length > 0) {
        pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey";
      }
      path = utils.encodeUri(pathTemplate, pathParams);
    } else if (event.isRedaction()) {
      const pathTemplate = `/rooms/$roomId/redact/$redactsEventId/$txnId`;
      path = utils.encodeUri(pathTemplate, _objectSpread({
        $redactsEventId: event.event.redacts
      }, pathParams));
    } else {
      path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", pathParams);
    }
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, event.getWireContent()).then(res => {
      _logger.logger.log(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`);
      return res;
    });
  }

  /**
   * @param txnId -  transaction id. One will be made up if not supplied.
   * @param opts - Options to pass on, may contain `reason` and `with_relations` (MSC3912)
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   * @throws Error if called with `with_relations` (MSC3912) but the server does not support it.
   *         Callers should check whether the server supports MSC3912 via `MatrixClient.canSupport`.
   */

  redactEvent(roomId, threadId, eventId, txnId, opts) {
    var _eventId, _opts2, _opts3, _opts4, _opts5;
    if (!((_eventId = eventId) !== null && _eventId !== void 0 && _eventId.startsWith(EVENT_ID_PREFIX))) {
      opts = txnId;
      txnId = eventId;
      eventId = threadId;
      threadId = null;
    }
    const reason = (_opts2 = opts) === null || _opts2 === void 0 ? void 0 : _opts2.reason;
    if ((_opts3 = opts) !== null && _opts3 !== void 0 && _opts3.with_relations && this.canSupport.get(_feature.Feature.RelationBasedRedactions) === _feature.ServerSupport.Unsupported) {
      throw new Error("Server does not support relation based redactions " + `roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId ${threadId}`);
    }
    const withRelations = (_opts4 = opts) !== null && _opts4 !== void 0 && _opts4.with_relations ? {
      [this.canSupport.get(_feature.Feature.RelationBasedRedactions) === _feature.ServerSupport.Stable ? _event2.MSC3912_RELATION_BASED_REDACTIONS_PROP.stable : _event2.MSC3912_RELATION_BASED_REDACTIONS_PROP.unstable]: (_opts5 = opts) === null || _opts5 === void 0 ? void 0 : _opts5.with_relations
    } : {};
    return this.sendCompleteEvent(roomId, threadId, {
      type: _event2.EventType.RoomRedaction,
      content: _objectSpread(_objectSpread({}, withRelations), {}, {
        reason
      }),
      redacts: eventId
    }, txnId);
  }

  /**
   * @param txnId - Optional.
   * @returns Promise which resolves: to an ISendEventResponse object
   * @returns Rejects: with an error response.
   */

  sendMessage(roomId, threadId, content, txnId) {
    if (typeof threadId !== "string" && threadId !== null) {
      txnId = content;
      content = threadId;
      threadId = null;
    }
    const eventType = _event2.EventType.RoomMessage;
    const sendContent = content;
    return this.sendEvent(roomId, threadId, eventType, sendContent, txnId);
  }

  /**
   * @param txnId - Optional.
   * @returns
   * @returns Rejects: with an error response.
   */

  sendTextMessage(roomId, threadId, body, txnId) {
    var _threadId;
    if (!((_threadId = threadId) !== null && _threadId !== void 0 && _threadId.startsWith(EVENT_ID_PREFIX)) && threadId !== null) {
      txnId = body;
      body = threadId;
      threadId = null;
    }
    const content = ContentHelpers.makeTextMessage(body);
    return this.sendMessage(roomId, threadId, content, txnId);
  }

  /**
   * @param txnId - Optional.
   * @returns Promise which resolves: to a ISendEventResponse object
   * @returns Rejects: with an error response.
   */

  sendNotice(roomId, threadId, body, txnId) {
    var _threadId2;
    if (!((_threadId2 = threadId) !== null && _threadId2 !== void 0 && _threadId2.startsWith(EVENT_ID_PREFIX)) && threadId !== null) {
      txnId = body;
      body = threadId;
      threadId = null;
    }
    const content = ContentHelpers.makeNotice(body);
    return this.sendMessage(roomId, threadId, content, txnId);
  }

  /**
   * @param txnId - Optional.
   * @returns Promise which resolves: to a ISendEventResponse object
   * @returns Rejects: with an error response.
   */

  sendEmoteMessage(roomId, threadId, body, txnId) {
    var _threadId3;
    if (!((_threadId3 = threadId) !== null && _threadId3 !== void 0 && _threadId3.startsWith(EVENT_ID_PREFIX)) && threadId !== null) {
      txnId = body;
      body = threadId;
      threadId = null;
    }
    const content = ContentHelpers.makeEmoteMessage(body);
    return this.sendMessage(roomId, threadId, content, txnId);
  }

  /**
   * @returns Promise which resolves: to a ISendEventResponse object
   * @returns Rejects: with an error response.
   */

  sendImageMessage(roomId, threadId, url, info, text = "Image") {
    var _threadId4;
    if (!((_threadId4 = threadId) !== null && _threadId4 !== void 0 && _threadId4.startsWith(EVENT_ID_PREFIX)) && threadId !== null) {
      text = info || "Image";
      info = url;
      url = threadId;
      threadId = null;
    }
    const content = {
      msgtype: _event2.MsgType.Image,
      url: url,
      info: info,
      body: text
    };
    return this.sendMessage(roomId, threadId, content);
  }

  /**
   * @returns Promise which resolves: to a ISendEventResponse object
   * @returns Rejects: with an error response.
   */

  sendStickerMessage(roomId, threadId, url, info, text = "Sticker") {
    var _threadId5;
    if (!((_threadId5 = threadId) !== null && _threadId5 !== void 0 && _threadId5.startsWith(EVENT_ID_PREFIX)) && threadId !== null) {
      text = info || "Sticker";
      info = url;
      url = threadId;
      threadId = null;
    }
    const content = {
      url: url,
      info: info,
      body: text
    };
    return this.sendEvent(roomId, threadId, _event2.EventType.Sticker, content);
  }

  /**
   * @returns Promise which resolves: to a ISendEventResponse object
   * @returns Rejects: with an error response.
   */

  sendHtmlMessage(roomId, threadId, body, htmlBody) {
    var _threadId6;
    if (!((_threadId6 = threadId) !== null && _threadId6 !== void 0 && _threadId6.startsWith(EVENT_ID_PREFIX)) && threadId !== null) {
      htmlBody = body;
      body = threadId;
      threadId = null;
    }
    const content = ContentHelpers.makeHtmlMessage(body, htmlBody);
    return this.sendMessage(roomId, threadId, content);
  }

  /**
   * @returns Promise which resolves: to a ISendEventResponse object
   * @returns Rejects: with an error response.
   */

  sendHtmlNotice(roomId, threadId, body, htmlBody) {
    var _threadId7;
    if (!((_threadId7 = threadId) !== null && _threadId7 !== void 0 && _threadId7.startsWith(EVENT_ID_PREFIX)) && threadId !== null) {
      htmlBody = body;
      body = threadId;
      threadId = null;
    }
    const content = ContentHelpers.makeHtmlNotice(body, htmlBody);
    return this.sendMessage(roomId, threadId, content);
  }

  /**
   * @returns Promise which resolves: to a ISendEventResponse object
   * @returns Rejects: with an error response.
   */

  sendHtmlEmote(roomId, threadId, body, htmlBody) {
    var _threadId8;
    if (!((_threadId8 = threadId) !== null && _threadId8 !== void 0 && _threadId8.startsWith(EVENT_ID_PREFIX)) && threadId !== null) {
      htmlBody = body;
      body = threadId;
      threadId = null;
    }
    const content = ContentHelpers.makeHtmlEmote(body, htmlBody);
    return this.sendMessage(roomId, threadId, content);
  }

  /**
   * Send a receipt.
   * @param event - The event being acknowledged
   * @param receiptType - The kind of receipt e.g. "m.read". Other than
   * ReceiptType.Read are experimental!
   * @param body - Additional content to send alongside the receipt.
   * @param unthreaded - An unthreaded receipt will clear room+thread notifications
   * @returns Promise which resolves: to an empty object `{}`
   * @returns Rejects: with an error response.
   */
  async sendReceipt(event, receiptType, body, unthreaded = false) {
    if (this.isGuest()) {
      return Promise.resolve({}); // guests cannot send receipts so don't bother.
    }

    const path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
      $roomId: event.getRoomId(),
      $receiptType: receiptType,
      $eventId: event.getId()
    });
    if (!unthreaded) {
      const isThread = !!event.threadRootId;
      body = _objectSpread(_objectSpread({}, body), {}, {
        thread_id: isThread ? event.threadRootId : _read_receipts.MAIN_ROOM_TIMELINE
      });
    }
    const promise = this.http.authedRequest(_httpApi.Method.Post, path, undefined, body || {});
    const room = this.getRoom(event.getRoomId());
    if (room && this.credentials.userId) {
      room.addLocalEchoReceipt(this.credentials.userId, event, receiptType);
    }
    return promise;
  }

  /**
   * Send a read receipt.
   * @param event - The event that has been read.
   * @param receiptType - other than ReceiptType.Read are experimental! Optional.
   * @returns Promise which resolves: to an empty object `{}`
   * @returns Rejects: with an error response.
   */
  async sendReadReceipt(event, receiptType = _read_receipts.ReceiptType.Read, unthreaded = false) {
    if (!event) return;
    const eventId = event.getId();
    const room = this.getRoom(event.getRoomId());
    if (room !== null && room !== void 0 && room.hasPendingEvent(eventId)) {
      throw new Error(`Cannot set read receipt to a pending event (${eventId})`);
    }
    return this.sendReceipt(event, receiptType, {}, unthreaded);
  }

  /**
   * Set a marker to indicate the point in a room before which the user has read every
   * event. This can be retrieved from room account data (the event type is `m.fully_read`)
   * and displayed as a horizontal line in the timeline that is visually distinct to the
   * position of the user's own read receipt.
   * @param roomId - ID of the room that has been read
   * @param rmEventId - ID of the event that has been read
   * @param rrEvent - the event tracked by the read receipt. This is here for
   * convenience because the RR and the RM are commonly updated at the same time as each
   * other. The local echo of this receipt will be done if set. Optional.
   * @param rpEvent - the m.read.private read receipt event for when we don't
   * want other users to see the read receipts. This is experimental. Optional.
   * @returns Promise which resolves: the empty object, `{}`.
   */
  async setRoomReadMarkers(roomId, rmEventId, rrEvent, rpEvent) {
    const room = this.getRoom(roomId);
    if (room && room.hasPendingEvent(rmEventId)) {
      throw new Error(`Cannot set read marker to a pending event (${rmEventId})`);
    }

    // Add the optional RR update, do local echo like `sendReceipt`
    let rrEventId;
    if (rrEvent) {
      rrEventId = rrEvent.getId();
      if (room !== null && room !== void 0 && room.hasPendingEvent(rrEventId)) {
        throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`);
      }
      room === null || room === void 0 ? void 0 : room.addLocalEchoReceipt(this.credentials.userId, rrEvent, _read_receipts.ReceiptType.Read);
    }

    // Add the optional private RR update, do local echo like `sendReceipt`
    let rpEventId;
    if (rpEvent) {
      rpEventId = rpEvent.getId();
      if (room !== null && room !== void 0 && room.hasPendingEvent(rpEventId)) {
        throw new Error(`Cannot set read receipt to a pending event (${rpEventId})`);
      }
      room === null || room === void 0 ? void 0 : room.addLocalEchoReceipt(this.credentials.userId, rpEvent, _read_receipts.ReceiptType.ReadPrivate);
    }
    return await this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, rpEventId);
  }

  /**
   * Get a preview of the given URL as of (roughly) the given point in time,
   * described as an object with OpenGraph keys and associated values.
   * Attributes may be synthesized where actual OG metadata is lacking.
   * Caches results to prevent hammering the server.
   * @param url - The URL to get preview data for
   * @param ts - The preferred point in time that the preview should
   * describe (ms since epoch).  The preview returned will either be the most
   * recent one preceding this timestamp if available, or failing that the next
   * most recent available preview.
   * @returns Promise which resolves: Object of OG metadata.
   * @returns Rejects: with an error response.
   * May return synthesized attributes if the URL lacked OG meta.
   */
  getUrlPreview(url, ts) {
    // bucket the timestamp to the nearest minute to prevent excessive spam to the server
    // Surely 60-second accuracy is enough for anyone.
    ts = Math.floor(ts / 60000) * 60000;
    const parsed = new URL(url);
    parsed.hash = ""; // strip the hash as it won't affect the preview
    url = parsed.toString();
    const key = ts + "_" + url;

    // If there's already a request in flight (or we've handled it), return that instead.
    const cachedPreview = this.urlPreviewCache[key];
    if (cachedPreview) {
      return cachedPreview;
    }
    const resp = this.http.authedRequest(_httpApi.Method.Get, "/preview_url", {
      url,
      ts: ts.toString()
    }, undefined, {
      prefix: _httpApi.MediaPrefix.R0
    });
    // TODO: Expire the URL preview cache sometimes
    this.urlPreviewCache[key] = resp;
    return resp;
  }

  /**
   * @returns Promise which resolves: to an empty object `{}`
   * @returns Rejects: with an error response.
   */
  sendTyping(roomId, isTyping, timeoutMs) {
    if (this.isGuest()) {
      return Promise.resolve({}); // guests cannot send typing notifications so don't bother.
    }

    const path = utils.encodeUri("/rooms/$roomId/typing/$userId", {
      $roomId: roomId,
      $userId: this.getUserId()
    });
    const data = {
      typing: isTyping
    };
    if (isTyping) {
      data.timeout = timeoutMs ? timeoutMs : 20000;
    }
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, data);
  }

  /**
   * Determines the history of room upgrades for a given room, as far as the
   * client can see. Returns an array of Rooms where the first entry is the
   * oldest and the last entry is the newest (likely current) room. If the
   * provided room is not found, this returns an empty list. This works in
   * both directions, looking for older and newer rooms of the given room.
   * @param roomId - The room ID to search from
   * @param verifyLinks - If true, the function will only return rooms
   * which can be proven to be linked. For example, rooms which have a create
   * event pointing to an old room which the client is not aware of or doesn't
   * have a matching tombstone would not be returned.
   * @param msc3946ProcessDynamicPredecessor - if true, look for
   * m.room.predecessor state events as well as create events, and prefer
   * predecessor events where they exist (MSC3946).
   * @returns An array of rooms representing the upgrade
   * history.
   */
  getRoomUpgradeHistory(roomId, verifyLinks = false, msc3946ProcessDynamicPredecessor = false) {
    const currentRoom = this.getRoom(roomId);
    if (!currentRoom) return [];
    const before = this.findPredecessorRooms(currentRoom, verifyLinks, msc3946ProcessDynamicPredecessor);
    const after = this.findSuccessorRooms(currentRoom, verifyLinks, msc3946ProcessDynamicPredecessor);
    return [...before, currentRoom, ...after];
  }
  findPredecessorRooms(room, verifyLinks, msc3946ProcessDynamicPredecessor) {
    var _room$findPredecessor;
    const ret = [];

    // Work backwards from newer to older rooms
    let predecessorRoomId = (_room$findPredecessor = room.findPredecessor(msc3946ProcessDynamicPredecessor)) === null || _room$findPredecessor === void 0 ? void 0 : _room$findPredecessor.roomId;
    while (predecessorRoomId !== null) {
      var _room$findPredecessor2;
      const predecessorRoom = this.getRoom(predecessorRoomId);
      if (predecessorRoom === null) {
        break;
      }
      if (verifyLinks) {
        const tombstone = predecessorRoom.currentState.getStateEvents(_event2.EventType.RoomTombstone, "");
        if (!tombstone || tombstone.getContent()["replacement_room"] !== room.roomId) {
          break;
        }
      }

      // Insert at the front because we're working backwards from the currentRoom
      ret.splice(0, 0, predecessorRoom);
      room = predecessorRoom;
      predecessorRoomId = (_room$findPredecessor2 = room.findPredecessor(msc3946ProcessDynamicPredecessor)) === null || _room$findPredecessor2 === void 0 ? void 0 : _room$findPredecessor2.roomId;
    }
    return ret;
  }
  findSuccessorRooms(room, verifyLinks, msc3946ProcessDynamicPredecessor) {
    const ret = [];

    // Work forwards, looking at tombstone events
    let tombstoneEvent = room.currentState.getStateEvents(_event2.EventType.RoomTombstone, "");
    while (tombstoneEvent) {
      const successorRoom = this.getRoom(tombstoneEvent.getContent()["replacement_room"]);
      if (!successorRoom) break; // end of the chain
      if (successorRoom.roomId === room.roomId) break; // Tombstone is referencing its own room

      if (verifyLinks) {
        var _successorRoom$findPr;
        const predecessorRoomId = (_successorRoom$findPr = successorRoom.findPredecessor(msc3946ProcessDynamicPredecessor)) === null || _successorRoom$findPr === void 0 ? void 0 : _successorRoom$findPr.roomId;
        if (!predecessorRoomId || predecessorRoomId !== room.roomId) {
          break;
        }
      }

      // Push to the end because we're looking forwards
      ret.push(successorRoom);
      const roomIds = new Set(ret.map(ref => ref.roomId));
      if (roomIds.size < ret.length) {
        // The last room added to the list introduced a previous roomId
        // To avoid recursion, return the last rooms - 1
        return ret.slice(0, ret.length - 1);
      }

      // Set the current room to the reference room so we know where we're at
      room = successorRoom;
      tombstoneEvent = room.currentState.getStateEvents(_event2.EventType.RoomTombstone, "");
    }
    return ret;
  }

  /**
   * @param reason - Optional.
   * @returns Promise which resolves: `{}` an empty object.
   * @returns Rejects: with an error response.
   */
  invite(roomId, userId, reason) {
    return this.membershipChange(roomId, userId, "invite", reason);
  }

  /**
   * Invite a user to a room based on their email address.
   * @param roomId - The room to invite the user to.
   * @param email - The email address to invite.
   * @returns Promise which resolves: `{}` an empty object.
   * @returns Rejects: with an error response.
   */
  inviteByEmail(roomId, email) {
    return this.inviteByThreePid(roomId, "email", email);
  }

  /**
   * Invite a user to a room based on a third-party identifier.
   * @param roomId - The room to invite the user to.
   * @param medium - The medium to invite the user e.g. "email".
   * @param address - The address for the specified medium.
   * @returns Promise which resolves: `{}` an empty object.
   * @returns Rejects: with an error response.
   */
  async inviteByThreePid(roomId, medium, address) {
    var _this$identityServer;
    const path = utils.encodeUri("/rooms/$roomId/invite", {
      $roomId: roomId
    });
    const identityServerUrl = this.getIdentityServerUrl(true);
    if (!identityServerUrl) {
      return Promise.reject(new _httpApi.MatrixError({
        error: "No supplied identity server URL",
        errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM"
      }));
    }
    const params = {
      id_server: identityServerUrl,
      medium: medium,
      address: address
    };
    if ((_this$identityServer = this.identityServer) !== null && _this$identityServer !== void 0 && _this$identityServer.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) {
      const identityAccessToken = await this.identityServer.getAccessToken();
      if (identityAccessToken) {
        params["id_access_token"] = identityAccessToken;
      }
    }
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, params);
  }

  /**
   * @returns Promise which resolves: `{}` an empty object.
   * @returns Rejects: with an error response.
   */
  leave(roomId) {
    return this.membershipChange(roomId, undefined, "leave");
  }

  /**
   * Leaves all rooms in the chain of room upgrades based on the given room. By
   * default, this will leave all the previous and upgraded rooms, including the
   * given room. To only leave the given room and any previous rooms, keeping the
   * upgraded (modern) rooms untouched supply `false` to `includeFuture`.
   * @param roomId - The room ID to start leaving at
   * @param includeFuture - If true, the whole chain (past and future) of
   * upgraded rooms will be left.
   * @returns Promise which resolves when completed with an object keyed
   * by room ID and value of the error encountered when leaving or null.
   */
  leaveRoomChain(roomId, includeFuture = true) {
    const upgradeHistory = this.getRoomUpgradeHistory(roomId);
    let eligibleToLeave = upgradeHistory;
    if (!includeFuture) {
      eligibleToLeave = [];
      for (const room of upgradeHistory) {
        eligibleToLeave.push(room);
        if (room.roomId === roomId) {
          break;
        }
      }
    }
    const populationResults = {};
    const promises = [];
    const doLeave = roomId => {
      return this.leave(roomId).then(() => {
        delete populationResults[roomId];
      }).catch(err => {
        // suppress error
        populationResults[roomId] = err;
      });
    };
    for (const room of eligibleToLeave) {
      promises.push(doLeave(room.roomId));
    }
    return Promise.all(promises).then(() => populationResults);
  }

  /**
   * @param reason - Optional.
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  ban(roomId, userId, reason) {
    return this.membershipChange(roomId, userId, "ban", reason);
  }

  /**
   * @param deleteRoom - True to delete the room from the store on success.
   * Default: true.
   * @returns Promise which resolves: `{}` an empty object.
   * @returns Rejects: with an error response.
   */
  forget(roomId, deleteRoom = true) {
    const promise = this.membershipChange(roomId, undefined, "forget");
    if (!deleteRoom) {
      return promise;
    }
    return promise.then(response => {
      this.store.removeRoom(roomId);
      this.emit(ClientEvent.DeleteRoom, roomId);
      return response;
    });
  }

  /**
   * @returns Promise which resolves: Object (currently empty)
   * @returns Rejects: with an error response.
   */
  unban(roomId, userId) {
    // unbanning != set their state to leave: this used to be
    // the case, but was then changed so that leaving was always
    // a revoking of privilege, otherwise two people racing to
    // kick / ban someone could end up banning and then un-banning
    // them.
    const path = utils.encodeUri("/rooms/$roomId/unban", {
      $roomId: roomId
    });
    const data = {
      user_id: userId
    };
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data);
  }

  /**
   * @param reason - Optional.
   * @returns Promise which resolves: `{}` an empty object.
   * @returns Rejects: with an error response.
   */
  kick(roomId, userId, reason) {
    const path = utils.encodeUri("/rooms/$roomId/kick", {
      $roomId: roomId
    });
    const data = {
      user_id: userId,
      reason: reason
    };
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data);
  }
  membershipChange(roomId, userId, membership, reason) {
    // API returns an empty object
    const path = utils.encodeUri("/rooms/$room_id/$membership", {
      $room_id: roomId,
      $membership: membership
    });
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, {
      user_id: userId,
      // may be undefined e.g. on leave
      reason: reason
    });
  }

  /**
   * Obtain a dict of actions which should be performed for this event according
   * to the push rules for this user.  Caches the dict on the event.
   * @param event - The event to get push actions for.
   * @param forceRecalculate - forces to recalculate actions for an event
   * Useful when an event just got decrypted
   * @returns A dict of actions to perform.
   */
  getPushActionsForEvent(event, forceRecalculate = false) {
    if (!event.getPushActions() || forceRecalculate) {
      event.setPushActions(this.pushProcessor.actionsForEvent(event));
    }
    return event.getPushActions();
  }

  /**
   * @param info - The kind of info to set (e.g. 'avatar_url')
   * @param data - The JSON object to set.
   * @returns
   * @returns Rejects: with an error response.
   */
  // eslint-disable-next-line camelcase

  setProfileInfo(info, data) {
    const path = utils.encodeUri("/profile/$userId/$info", {
      $userId: this.credentials.userId,
      $info: info
    });
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, data);
  }

  /**
   * @returns Promise which resolves: `{}` an empty object.
   * @returns Rejects: with an error response.
   */
  async setDisplayName(name) {
    const prom = await this.setProfileInfo("displayname", {
      displayname: name
    });
    // XXX: synthesise a profile update for ourselves because Synapse is broken and won't
    const user = this.getUser(this.getUserId());
    if (user) {
      user.displayName = name;
      user.emit(_user.UserEvent.DisplayName, user.events.presence, user);
    }
    return prom;
  }

  /**
   * @returns Promise which resolves: `{}` an empty object.
   * @returns Rejects: with an error response.
   */
  async setAvatarUrl(url) {
    const prom = await this.setProfileInfo("avatar_url", {
      avatar_url: url
    });
    // XXX: synthesise a profile update for ourselves because Synapse is broken and won't
    const user = this.getUser(this.getUserId());
    if (user) {
      user.avatarUrl = url;
      user.emit(_user.UserEvent.AvatarUrl, user.events.presence, user);
    }
    return prom;
  }

  /**
   * Turn an MXC URL into an HTTP one. <strong>This method is experimental and
   * may change.</strong>
   * @param mxcUrl - The MXC URL
   * @param width - The desired width of the thumbnail.
   * @param height - The desired height of the thumbnail.
   * @param resizeMethod - The thumbnail resize method to use, either
   * "crop" or "scale".
   * @param allowDirectLinks - If true, return any non-mxc URLs
   * directly. Fetching such URLs will leak information about the user to
   * anyone they share a room with. If false, will return null for such URLs.
   * @returns the avatar URL or null.
   */
  mxcUrlToHttp(mxcUrl, width, height, resizeMethod, allowDirectLinks) {
    return (0, _contentRepo.getHttpUriForMxc)(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks);
  }

  /**
   * @param opts - Options to apply
   * @returns Promise which resolves
   * @returns Rejects: with an error response.
   * @throws If 'presence' isn't a valid presence enum value.
   */
  async setPresence(opts) {
    const path = utils.encodeUri("/presence/$userId/status", {
      $userId: this.credentials.userId
    });
    const validStates = ["offline", "online", "unavailable"];
    if (validStates.indexOf(opts.presence) === -1) {
      throw new Error("Bad presence value: " + opts.presence);
    }
    await this.http.authedRequest(_httpApi.Method.Put, path, undefined, opts);
  }

  /**
   * @param userId - The user to get presence for
   * @returns Promise which resolves: The presence state for this user.
   * @returns Rejects: with an error response.
   */
  getPresence(userId) {
    const path = utils.encodeUri("/presence/$userId/status", {
      $userId: userId
    });
    return this.http.authedRequest(_httpApi.Method.Get, path);
  }

  /**
   * Retrieve older messages from the given room and put them in the timeline.
   *
   * If this is called multiple times whilst a request is ongoing, the <i>same</i>
   * Promise will be returned. If there was a problem requesting scrollback, there
   * will be a small delay before another request can be made (to prevent tight-looping
   * when there is no connection).
   *
   * @param room - The room to get older messages in.
   * @param limit - Optional. The maximum number of previous events to
   * pull in. Default: 30.
   * @returns Promise which resolves: Room. If you are at the beginning
   * of the timeline, `Room.oldState.paginationToken` will be
   * `null`.
   * @returns Rejects: with an error response.
   */
  scrollback(room, limit = 30) {
    let timeToWaitMs = 0;
    let info = this.ongoingScrollbacks[room.roomId] || {};
    if (info.promise) {
      return info.promise;
    } else if (info.errorTs) {
      const timeWaitedMs = Date.now() - info.errorTs;
      timeToWaitMs = Math.max(SCROLLBACK_DELAY_MS - timeWaitedMs, 0);
    }
    if (room.oldState.paginationToken === null) {
      return Promise.resolve(room); // already at the start.
    }
    // attempt to grab more events from the store first
    const numAdded = this.store.scrollback(room, limit).length;
    if (numAdded === limit) {
      // store contained everything we needed.
      return Promise.resolve(room);
    }
    // reduce the required number of events appropriately
    limit = limit - numAdded;
    const promise = new Promise((resolve, reject) => {
      // wait for a time before doing this request
      // (which may be 0 in order not to special case the code paths)
      (0, utils.sleep)(timeToWaitMs).then(() => {
        return this.createMessagesRequest(room.roomId, room.oldState.paginationToken, limit, _eventTimeline.Direction.Backward);
      }).then(res => {
        var _res$end, _res$end2;
        const matrixEvents = res.chunk.map(this.getEventMapper());
        if (res.state) {
          const stateEvents = res.state.map(this.getEventMapper());
          room.currentState.setUnknownStateEvents(stateEvents);
        }
        const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
        this.processAggregatedTimelineEvents(room, timelineEvents);
        room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
        this.processThreadEvents(room, threadedEvents, true);
        room.oldState.paginationToken = (_res$end = res.end) !== null && _res$end !== void 0 ? _res$end : null;
        if (res.chunk.length === 0) {
          room.oldState.paginationToken = null;
        }
        this.store.storeEvents(room, matrixEvents, (_res$end2 = res.end) !== null && _res$end2 !== void 0 ? _res$end2 : null, true);
        delete this.ongoingScrollbacks[room.roomId];
        resolve(room);
      }).catch(err => {
        this.ongoingScrollbacks[room.roomId] = {
          errorTs: Date.now()
        };
        reject(err);
      });
    });
    info = {
      promise
    };
    this.ongoingScrollbacks[room.roomId] = info;
    return promise;
  }
  getEventMapper(options) {
    return (0, _eventMapper.eventMapperFor)(this, options || {});
  }

  /**
   * Get an EventTimeline for the given event
   *
   * <p>If the EventTimelineSet object already has the given event in its store, the
   * corresponding timeline will be returned. Otherwise, a /context request is
   * made, and used to construct an EventTimeline.
   * If the event does not belong to this EventTimelineSet then undefined will be returned.
   *
   * @param timelineSet -  The timelineSet to look for the event in, must be bound to a room
   * @param eventId -  The ID of the event to look for
   *
   * @returns Promise which resolves:
   *    {@link EventTimeline} including the given event
   */
  async getEventTimeline(timelineSet, eventId) {
    var _this$clientOpts, _ref, _timelineSet$getTimel, _timelineSet$room$fin;
    // don't allow any timeline support unless it's been enabled.
    if (!this.timelineSupport) {
      throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable it.");
    }
    if (!(timelineSet !== null && timelineSet !== void 0 && timelineSet.room)) {
      throw new Error("getEventTimeline only supports room timelines");
    }
    if (timelineSet.getTimelineForEvent(eventId)) {
      return timelineSet.getTimelineForEvent(eventId);
    }
    if (timelineSet.thread && this.supportsThreads()) {
      return this.getThreadTimeline(timelineSet, eventId);
    }
    const path = utils.encodeUri("/rooms/$roomId/context/$eventId", {
      $roomId: timelineSet.room.roomId,
      $eventId: eventId
    });
    let params = undefined;
    if ((_this$clientOpts = this.clientOpts) !== null && _this$clientOpts !== void 0 && _this$clientOpts.lazyLoadMembers) {
      params = {
        filter: JSON.stringify(_filter.Filter.LAZY_LOADING_MESSAGES_FILTER)
      };
    }

    // TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors.
    const res = await this.http.authedRequest(_httpApi.Method.Get, path, params);
    if (!res.event) {
      throw new Error("'event' not in '/context' result - homeserver too old?");
    }

    // by the time the request completes, the event might have ended up in the timeline.
    if (timelineSet.getTimelineForEvent(eventId)) {
      return timelineSet.getTimelineForEvent(eventId);
    }
    const mapper = this.getEventMapper();
    const event = mapper(res.event);
    if (event.isRelation(_thread.THREAD_RELATION_TYPE.name)) {
      _logger.logger.warn("Tried loading a regular timeline at the position of a thread event");
      return undefined;
    }
    const events = [
    // Order events from most recent to oldest (reverse-chronological).
    // We start with the last event, since that's the point at which we have known state.
    // events_after is already backwards; events_before is forwards.
    ...res.events_after.reverse().map(mapper), event, ...res.events_before.map(mapper)];

    // Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
    let timeline = timelineSet.getTimelineForEvent(events[0].getId());
    if (timeline) {
      timeline.getState(_eventTimeline.EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
    } else {
      timeline = timelineSet.addTimeline();
      timeline.initialiseState(res.state.map(mapper));
      timeline.getState(_eventTimeline.EventTimeline.FORWARDS).paginationToken = res.end;
    }
    const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
    timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
    // The target event is not in a thread but process the contextual events, so we can show any threads around it.
    this.processThreadEvents(timelineSet.room, threadedEvents, true);
    this.processAggregatedTimelineEvents(timelineSet.room, timelineEvents);

    // There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
    // timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
    // anywhere, if it was later redacted, so we just return the timeline we first thought of.
    return (_ref = (_timelineSet$getTimel = timelineSet.getTimelineForEvent(eventId)) !== null && _timelineSet$getTimel !== void 0 ? _timelineSet$getTimel : (_timelineSet$room$fin = timelineSet.room.findThreadForEvent(event)) === null || _timelineSet$room$fin === void 0 ? void 0 : _timelineSet$room$fin.liveTimeline) !== null && _ref !== void 0 ? _ref :
    // for Threads degraded support
    timeline;
  }
  async getThreadTimeline(timelineSet, eventId) {
    var _this$clientOpts2;
    if (!this.supportsThreads()) {
      throw new Error("could not get thread timeline: no client support");
    }
    if (!timelineSet.room) {
      throw new Error("could not get thread timeline: not a room timeline");
    }
    if (!timelineSet.thread) {
      throw new Error("could not get thread timeline: not a thread timeline");
    }
    const path = utils.encodeUri("/rooms/$roomId/context/$eventId", {
      $roomId: timelineSet.room.roomId,
      $eventId: eventId
    });
    const params = {
      limit: "0"
    };
    if ((_this$clientOpts2 = this.clientOpts) !== null && _this$clientOpts2 !== void 0 && _this$clientOpts2.lazyLoadMembers) {
      params.filter = JSON.stringify(_filter.Filter.LAZY_LOADING_MESSAGES_FILTER);
    }

    // TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors.
    const res = await this.http.authedRequest(_httpApi.Method.Get, path, params);
    const mapper = this.getEventMapper();
    const event = mapper(res.event);
    if (!timelineSet.canContain(event)) {
      return undefined;
    }
    if (_thread.Thread.hasServerSideSupport) {
      if (_thread.Thread.hasServerSideFwdPaginationSupport) {
        var _resOlder$next_batch, _resNewer$next_batch, _timelineSet$getTimel2;
        if (!timelineSet.thread) {
          throw new Error("could not get thread timeline: not a thread timeline");
        }
        const thread = timelineSet.thread;
        const resOlder = await this.fetchRelations(timelineSet.room.roomId, thread.id, _thread.THREAD_RELATION_TYPE.name, null, {
          dir: _eventTimeline.Direction.Backward,
          from: res.start
        });
        const resNewer = await this.fetchRelations(timelineSet.room.roomId, thread.id, _thread.THREAD_RELATION_TYPE.name, null, {
          dir: _eventTimeline.Direction.Forward,
          from: res.end
        });
        const events = [
        // Order events from most recent to oldest (reverse-chronological).
        // We start with the last event, since that's the point at which we have known state.
        // events_after is already backwards; events_before is forwards.
        ...resNewer.chunk.reverse().map(mapper), event, ...resOlder.chunk.map(mapper)];
        for (const event of events) {
          var _timelineSet$thread;
          await ((_timelineSet$thread = timelineSet.thread) === null || _timelineSet$thread === void 0 ? void 0 : _timelineSet$thread.processEvent(event));
        }

        // Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
        let timeline = timelineSet.getTimelineForEvent(event.getId());
        if (timeline) {
          timeline.getState(_eventTimeline.EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
        } else {
          timeline = timelineSet.addTimeline();
          timeline.initialiseState(res.state.map(mapper));
        }
        timelineSet.addEventsToTimeline(events, true, timeline, resNewer.next_batch);
        if (!resOlder.next_batch) {
          const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
          timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null);
        }
        timeline.setPaginationToken((_resOlder$next_batch = resOlder.next_batch) !== null && _resOlder$next_batch !== void 0 ? _resOlder$next_batch : null, _eventTimeline.Direction.Backward);
        timeline.setPaginationToken((_resNewer$next_batch = resNewer.next_batch) !== null && _resNewer$next_batch !== void 0 ? _resNewer$next_batch : null, _eventTimeline.Direction.Forward);
        this.processAggregatedTimelineEvents(timelineSet.room, events);

        // There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
        // timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
        // anywhere, if it was later redacted, so we just return the timeline we first thought of.
        return (_timelineSet$getTimel2 = timelineSet.getTimelineForEvent(eventId)) !== null && _timelineSet$getTimel2 !== void 0 ? _timelineSet$getTimel2 : timeline;
      } else {
        var _resOlder$next_batch2;
        // Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only
        // functions contiguously, so we have to jump through some hoops to get our target event in it.
        // XXX: workaround for https://github.com/vector-im/element-meta/issues/150

        const thread = timelineSet.thread;
        const resOlder = await this.fetchRelations(timelineSet.room.roomId, thread.id, _thread.THREAD_RELATION_TYPE.name, null, {
          dir: _eventTimeline.Direction.Backward,
          from: res.start
        });
        const eventsNewer = [];
        let nextBatch = res.end;
        while (nextBatch) {
          var _resNewer$next_batch2;
          const resNewer = await this.fetchRelations(timelineSet.room.roomId, thread.id, _thread.THREAD_RELATION_TYPE.name, null, {
            dir: _eventTimeline.Direction.Forward,
            from: nextBatch
          });
          nextBatch = (_resNewer$next_batch2 = resNewer.next_batch) !== null && _resNewer$next_batch2 !== void 0 ? _resNewer$next_batch2 : null;
          eventsNewer.push(...resNewer.chunk);
        }
        const events = [
        // Order events from most recent to oldest (reverse-chronological).
        // We start with the last event, since that's the point at which we have known state.
        // events_after is already backwards; events_before is forwards.
        ...eventsNewer.reverse().map(mapper), event, ...resOlder.chunk.map(mapper)];
        for (const event of events) {
          var _timelineSet$thread2;
          await ((_timelineSet$thread2 = timelineSet.thread) === null || _timelineSet$thread2 === void 0 ? void 0 : _timelineSet$thread2.processEvent(event));
        }

        // Here we handle non-thread timelines only, but still process any thread events to populate thread
        // summaries.
        const timeline = timelineSet.getLiveTimeline();
        timeline.getState(_eventTimeline.EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
        timelineSet.addEventsToTimeline(events, true, timeline, null);
        if (!resOlder.next_batch) {
          const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
          timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null);
        }
        timeline.setPaginationToken((_resOlder$next_batch2 = resOlder.next_batch) !== null && _resOlder$next_batch2 !== void 0 ? _resOlder$next_batch2 : null, _eventTimeline.Direction.Backward);
        timeline.setPaginationToken(null, _eventTimeline.Direction.Forward);
        this.processAggregatedTimelineEvents(timelineSet.room, events);
        return timeline;
      }
    }
  }

  /**
   * Get an EventTimeline for the latest events in the room. This will just
   * call `/messages` to get the latest message in the room, then use
   * `client.getEventTimeline(...)` to construct a new timeline from it.
   *
   * @param timelineSet -  The timelineSet to find or add the timeline to
   *
   * @returns Promise which resolves:
   *    {@link EventTimeline} timeline with the latest events in the room
   */
  async getLatestTimeline(timelineSet) {
    // don't allow any timeline support unless it's been enabled.
    if (!this.timelineSupport) {
      throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable it.");
    }
    if (!timelineSet.room) {
      throw new Error("getLatestTimeline only supports room timelines");
    }
    let event;
    if (timelineSet.threadListType !== null) {
      var _res$chunk;
      const res = await this.createThreadListMessagesRequest(timelineSet.room.roomId, null, 1, _eventTimeline.Direction.Backward, timelineSet.threadListType, timelineSet.getFilter());
      event = (_res$chunk = res.chunk) === null || _res$chunk === void 0 ? void 0 : _res$chunk[0];
    } else if (timelineSet.thread && _thread.Thread.hasServerSideSupport) {
      var _res$chunk2;
      const res = await this.fetchRelations(timelineSet.room.roomId, timelineSet.thread.id, _thread.THREAD_RELATION_TYPE.name, null, {
        dir: _eventTimeline.Direction.Backward,
        limit: 1
      });
      event = (_res$chunk2 = res.chunk) === null || _res$chunk2 === void 0 ? void 0 : _res$chunk2[0];
    } else {
      var _this$clientOpts3, _res$chunk3;
      const messagesPath = utils.encodeUri("/rooms/$roomId/messages", {
        $roomId: timelineSet.room.roomId
      });
      const params = {
        dir: "b"
      };
      if ((_this$clientOpts3 = this.clientOpts) !== null && _this$clientOpts3 !== void 0 && _this$clientOpts3.lazyLoadMembers) {
        params.filter = JSON.stringify(_filter.Filter.LAZY_LOADING_MESSAGES_FILTER);
      }
      const res = await this.http.authedRequest(_httpApi.Method.Get, messagesPath, params);
      event = (_res$chunk3 = res.chunk) === null || _res$chunk3 === void 0 ? void 0 : _res$chunk3[0];
    }
    if (!event) {
      throw new Error("No message returned when trying to construct getLatestTimeline");
    }
    return this.getEventTimeline(timelineSet, event.event_id);
  }

  /**
   * Makes a request to /messages with the appropriate lazy loading filter set.
   * XXX: if we do get rid of scrollback (as it's not used at the moment),
   * we could inline this method again in paginateEventTimeline as that would
   * then be the only call-site
   * @param limit - the maximum amount of events the retrieve
   * @param dir - 'f' or 'b'
   * @param timelineFilter - the timeline filter to pass
   */
  // XXX: Intended private, used in code.
  createMessagesRequest(roomId, fromToken, limit = 30, dir, timelineFilter) {
    var _this$clientOpts4;
    const path = utils.encodeUri("/rooms/$roomId/messages", {
      $roomId: roomId
    });
    const params = {
      limit: limit.toString(),
      dir: dir
    };
    if (fromToken) {
      params.from = fromToken;
    }
    let filter = null;
    if ((_this$clientOpts4 = this.clientOpts) !== null && _this$clientOpts4 !== void 0 && _this$clientOpts4.lazyLoadMembers) {
      // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
      // so the timelineFilter doesn't get written into it below
      filter = Object.assign({}, _filter.Filter.LAZY_LOADING_MESSAGES_FILTER);
    }
    if (timelineFilter) {
      var _timelineFilter$getRo;
      // XXX: it's horrific that /messages' filter parameter doesn't match
      // /sync's one - see https://matrix.org/jira/browse/SPEC-451
      filter = filter || {};
      Object.assign(filter, (_timelineFilter$getRo = timelineFilter.getRoomTimelineFilterComponent()) === null || _timelineFilter$getRo === void 0 ? void 0 : _timelineFilter$getRo.toJSON());
    }
    if (filter) {
      params.filter = JSON.stringify(filter);
    }
    return this.http.authedRequest(_httpApi.Method.Get, path, params);
  }

  /**
   * Makes a request to /messages with the appropriate lazy loading filter set.
   * XXX: if we do get rid of scrollback (as it's not used at the moment),
   * we could inline this method again in paginateEventTimeline as that would
   * then be the only call-site
   * @param limit - the maximum amount of events the retrieve
   * @param dir - 'f' or 'b'
   * @param timelineFilter - the timeline filter to pass
   */
  // XXX: Intended private, used by room.fetchRoomThreads
  createThreadListMessagesRequest(roomId, fromToken, limit = 30, dir = _eventTimeline.Direction.Backward, threadListType = _thread.ThreadFilterType.All, timelineFilter) {
    var _this$clientOpts5;
    const path = utils.encodeUri("/rooms/$roomId/threads", {
      $roomId: roomId
    });
    const params = {
      limit: limit.toString(),
      dir: dir,
      include: (0, _thread.threadFilterTypeToFilter)(threadListType)
    };
    if (fromToken) {
      params.from = fromToken;
    }
    let filter = {};
    if ((_this$clientOpts5 = this.clientOpts) !== null && _this$clientOpts5 !== void 0 && _this$clientOpts5.lazyLoadMembers) {
      // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
      // so the timelineFilter doesn't get written into it below
      filter = _objectSpread({}, _filter.Filter.LAZY_LOADING_MESSAGES_FILTER);
    }
    if (timelineFilter) {
      var _timelineFilter$getRo2;
      // XXX: it's horrific that /messages' filter parameter doesn't match
      // /sync's one - see https://matrix.org/jira/browse/SPEC-451
      filter = _objectSpread(_objectSpread({}, filter), (_timelineFilter$getRo2 = timelineFilter.getRoomTimelineFilterComponent()) === null || _timelineFilter$getRo2 === void 0 ? void 0 : _timelineFilter$getRo2.toJSON());
    }
    if (Object.keys(filter).length) {
      params.filter = JSON.stringify(filter);
    }
    const opts = {
      prefix: _thread.Thread.hasServerSideListSupport === _thread.FeatureSupport.Stable ? "/_matrix/client/v1" : "/_matrix/client/unstable/org.matrix.msc3856"
    };
    return this.http.authedRequest(_httpApi.Method.Get, path, params, undefined, opts).then(res => {
      var _res$chunk4;
      return _objectSpread(_objectSpread({}, res), {}, {
        chunk: (_res$chunk4 = res.chunk) === null || _res$chunk4 === void 0 ? void 0 : _res$chunk4.reverse(),
        start: res.prev_batch,
        end: res.next_batch
      });
    });
  }

  /**
   * Take an EventTimeline, and back/forward-fill results.
   *
   * @param eventTimeline - timeline object to be updated
   *
   * @returns Promise which resolves to a boolean: false if there are no
   *    events and we reached either end of the timeline; else true.
   */
  paginateEventTimeline(eventTimeline, opts) {
    const isNotifTimeline = eventTimeline.getTimelineSet() === this.notifTimelineSet;
    const room = this.getRoom(eventTimeline.getRoomId());
    const threadListType = eventTimeline.getTimelineSet().threadListType;
    const thread = eventTimeline.getTimelineSet().thread;

    // TODO: we should implement a backoff (as per scrollback()) to deal more
    // nicely with HTTP errors.
    opts = opts || {};
    const backwards = opts.backwards || false;
    if (isNotifTimeline) {
      if (!backwards) {
        throw new Error("paginateNotifTimeline can only paginate backwards");
      }
    }
    const dir = backwards ? _eventTimeline.EventTimeline.BACKWARDS : _eventTimeline.EventTimeline.FORWARDS;
    const token = eventTimeline.getPaginationToken(dir);
    const pendingRequest = eventTimeline.paginationRequests[dir];
    if (pendingRequest) {
      // already a request in progress - return the existing promise
      return pendingRequest;
    }
    let path;
    let params;
    let promise;
    if (isNotifTimeline) {
      var _opts$limit;
      path = "/notifications";
      params = {
        limit: ((_opts$limit = opts.limit) !== null && _opts$limit !== void 0 ? _opts$limit : 30).toString(),
        only: "highlight"
      };
      if (token && token !== "end") {
        params.from = token;
      }
      promise = this.http.authedRequest(_httpApi.Method.Get, path, params).then(async res => {
        const token = res.next_token;
        const matrixEvents = [];
        res.notifications = res.notifications.filter(utils.noUnsafeEventProps);
        for (let i = 0; i < res.notifications.length; i++) {
          const notification = res.notifications[i];
          const event = this.getEventMapper()(notification.event);
          event.setPushActions(_pushprocessor.PushProcessor.actionListToActionsObject(notification.actions));
          event.event.room_id = notification.room_id; // XXX: gutwrenching
          matrixEvents[i] = event;
        }

        // No need to partition events for threads here, everything lives
        // in the notification timeline set
        const timelineSet = eventTimeline.getTimelineSet();
        timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
        this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);

        // if we've hit the end of the timeline, we need to stop trying to
        // paginate. We need to keep the 'forwards' token though, to make sure
        // we can recover from gappy syncs.
        if (backwards && !res.next_token) {
          eventTimeline.setPaginationToken(null, dir);
        }
        return Boolean(res.next_token);
      }).finally(() => {
        eventTimeline.paginationRequests[dir] = null;
      });
      eventTimeline.paginationRequests[dir] = promise;
    } else if (threadListType !== null) {
      if (!room) {
        throw new Error("Unknown room " + eventTimeline.getRoomId());
      }
      if (!_thread.Thread.hasServerSideFwdPaginationSupport && dir === _eventTimeline.Direction.Forward) {
        throw new Error("Cannot paginate threads forwards without server-side support for MSC 3715");
      }
      promise = this.createThreadListMessagesRequest(eventTimeline.getRoomId(), token, opts.limit, dir, threadListType, eventTimeline.getFilter()).then(res => {
        if (res.state) {
          const roomState = eventTimeline.getState(dir);
          const stateEvents = res.state.filter(utils.noUnsafeEventProps).map(this.getEventMapper());
          roomState.setUnknownStateEvents(stateEvents);
        }
        const token = res.end;
        const matrixEvents = res.chunk.filter(utils.noUnsafeEventProps).map(this.getEventMapper());
        const timelineSet = eventTimeline.getTimelineSet();
        timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
        this.processAggregatedTimelineEvents(room, matrixEvents);
        this.processThreadRoots(room, matrixEvents, backwards);

        // if we've hit the end of the timeline, we need to stop trying to
        // paginate. We need to keep the 'forwards' token though, to make sure
        // we can recover from gappy syncs.
        if (backwards && res.end == res.start) {
          eventTimeline.setPaginationToken(null, dir);
        }
        return res.end !== res.start;
      }).finally(() => {
        eventTimeline.paginationRequests[dir] = null;
      });
      eventTimeline.paginationRequests[dir] = promise;
    } else if (thread) {
      var _eventTimeline$getRoo, _eventTimeline$getRoo2;
      const room = this.getRoom((_eventTimeline$getRoo = eventTimeline.getRoomId()) !== null && _eventTimeline$getRoo !== void 0 ? _eventTimeline$getRoo : undefined);
      if (!room) {
        throw new Error("Unknown room " + eventTimeline.getRoomId());
      }
      promise = this.fetchRelations((_eventTimeline$getRoo2 = eventTimeline.getRoomId()) !== null && _eventTimeline$getRoo2 !== void 0 ? _eventTimeline$getRoo2 : "", thread.id, _thread.THREAD_RELATION_TYPE.name, null, {
        dir,
        limit: opts.limit,
        from: token !== null && token !== void 0 ? token : undefined
      }).then(async res => {
        const mapper = this.getEventMapper();
        const matrixEvents = res.chunk.filter(utils.noUnsafeEventProps).map(mapper);

        // Process latest events first
        for (const event of matrixEvents.slice().reverse()) {
          await (thread === null || thread === void 0 ? void 0 : thread.processEvent(event));
          const sender = event.getSender();
          if (!backwards || (thread === null || thread === void 0 ? void 0 : thread.getEventReadUpTo(sender)) === null) {
            room.addLocalEchoReceipt(sender, event, _read_receipts.ReceiptType.Read);
          }
        }
        const newToken = res.next_batch;
        const timelineSet = eventTimeline.getTimelineSet();
        timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, newToken !== null && newToken !== void 0 ? newToken : null);
        if (!newToken && backwards) {
          var _eventTimeline$getRoo3;
          const originalEvent = await this.fetchRoomEvent((_eventTimeline$getRoo3 = eventTimeline.getRoomId()) !== null && _eventTimeline$getRoo3 !== void 0 ? _eventTimeline$getRoo3 : "", thread.id);
          timelineSet.addEventsToTimeline([mapper(originalEvent)], true, eventTimeline, null);
        }
        this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);

        // if we've hit the end of the timeline, we need to stop trying to
        // paginate. We need to keep the 'forwards' token though, to make sure
        // we can recover from gappy syncs.
        if (backwards && !newToken) {
          eventTimeline.setPaginationToken(null, dir);
        }
        return Boolean(newToken);
      }).finally(() => {
        eventTimeline.paginationRequests[dir] = null;
      });
      eventTimeline.paginationRequests[dir] = promise;
    } else {
      if (!room) {
        throw new Error("Unknown room " + eventTimeline.getRoomId());
      }
      promise = this.createMessagesRequest(eventTimeline.getRoomId(), token, opts.limit, dir, eventTimeline.getFilter()).then(res => {
        if (res.state) {
          const roomState = eventTimeline.getState(dir);
          const stateEvents = res.state.filter(utils.noUnsafeEventProps).map(this.getEventMapper());
          roomState.setUnknownStateEvents(stateEvents);
        }
        const token = res.end;
        const matrixEvents = res.chunk.filter(utils.noUnsafeEventProps).map(this.getEventMapper());
        const timelineSet = eventTimeline.getTimelineSet();
        const [timelineEvents] = room.partitionThreadedEvents(matrixEvents);
        timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
        this.processAggregatedTimelineEvents(room, timelineEvents);
        this.processThreadRoots(room, timelineEvents.filter(it => it.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name)), false);
        const atEnd = res.end === undefined || res.end === res.start;

        // if we've hit the end of the timeline, we need to stop trying to
        // paginate. We need to keep the 'forwards' token though, to make sure
        // we can recover from gappy syncs.
        if (backwards && atEnd) {
          eventTimeline.setPaginationToken(null, dir);
        }
        return !atEnd;
      }).finally(() => {
        eventTimeline.paginationRequests[dir] = null;
      });
      eventTimeline.paginationRequests[dir] = promise;
    }
    return promise;
  }

  /**
   * Reset the notifTimelineSet entirely, paginating in some historical notifs as
   * a starting point for subsequent pagination.
   */
  resetNotifTimelineSet() {
    if (!this.notifTimelineSet) {
      return;
    }

    // FIXME: This thing is a total hack, and results in duplicate events being
    // added to the timeline both from /sync and /notifications, and lots of
    // slow and wasteful processing and pagination.  The correct solution is to
    // extend /messages or /search or something to filter on notifications.

    // use the fictitious token 'end'. in practice we would ideally give it
    // the oldest backwards pagination token from /sync, but /sync doesn't
    // know about /notifications, so we have no choice but to start paginating
    // from the current point in time.  This may well overlap with historical
    // notifs which are then inserted into the timeline by /sync responses.
    this.notifTimelineSet.resetLiveTimeline("end");

    // we could try to paginate a single event at this point in order to get
    // a more valid pagination token, but it just ends up with an out of order
    // timeline. given what a mess this is and given we're going to have duplicate
    // events anyway, just leave it with the dummy token for now.
    /*
    this.paginateNotifTimeline(this._notifTimelineSet.getLiveTimeline(), {
        backwards: true,
        limit: 1
    });
    */
  }

  /**
   * Peek into a room and receive updates about the room. This only works if the
   * history visibility for the room is world_readable.
   * @param roomId - The room to attempt to peek into.
   * @returns Promise which resolves: Room object
   * @returns Rejects: with an error response.
   */
  peekInRoom(roomId) {
    var _this$peekSync2;
    (_this$peekSync2 = this.peekSync) === null || _this$peekSync2 === void 0 ? void 0 : _this$peekSync2.stopPeeking();
    this.peekSync = new _sync.SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
    return this.peekSync.peek(roomId);
  }

  /**
   * Stop any ongoing room peeking.
   */
  stopPeeking() {
    if (this.peekSync) {
      this.peekSync.stopPeeking();
      this.peekSync = null;
    }
  }

  /**
   * Set r/w flags for guest access in a room.
   * @param roomId - The room to configure guest access in.
   * @param opts - Options
   * @returns Promise which resolves
   * @returns Rejects: with an error response.
   */
  setGuestAccess(roomId, opts) {
    const writePromise = this.sendStateEvent(roomId, _event2.EventType.RoomGuestAccess, {
      guest_access: opts.allowJoin ? "can_join" : "forbidden"
    }, "");
    let readPromise = Promise.resolve(undefined);
    if (opts.allowRead) {
      readPromise = this.sendStateEvent(roomId, _event2.EventType.RoomHistoryVisibility, {
        history_visibility: "world_readable"
      }, "");
    }
    return Promise.all([readPromise, writePromise]).then(); // .then() to hide results for contract
  }

  /**
   * Requests an email verification token for the purposes of registration.
   * This API requests a token from the homeserver.
   * The doesServerRequireIdServerParam() method can be used to determine if
   * the server requires the id_server parameter to be provided.
   *
   * Parameters and return value are as for requestEmailToken
    * @param email - As requestEmailToken
   * @param clientSecret - As requestEmailToken
   * @param sendAttempt - As requestEmailToken
   * @param nextLink - As requestEmailToken
   * @returns Promise which resolves: As requestEmailToken
   */
  requestRegisterEmailToken(email, clientSecret, sendAttempt, nextLink) {
    return this.requestTokenFromEndpoint("/register/email/requestToken", {
      email: email,
      client_secret: clientSecret,
      send_attempt: sendAttempt,
      next_link: nextLink
    });
  }

  /**
   * Requests a text message verification token for the purposes of registration.
   * This API requests a token from the homeserver.
   * The doesServerRequireIdServerParam() method can be used to determine if
   * the server requires the id_server parameter to be provided.
   *
   * @param phoneCountry - The ISO 3166-1 alpha-2 code for the country in which
   *    phoneNumber should be parsed relative to.
   * @param phoneNumber - The phone number, in national or international format
   * @param clientSecret - As requestEmailToken
   * @param sendAttempt - As requestEmailToken
   * @param nextLink - As requestEmailToken
   * @returns Promise which resolves: As requestEmailToken
   */
  requestRegisterMsisdnToken(phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink) {
    return this.requestTokenFromEndpoint("/register/msisdn/requestToken", {
      country: phoneCountry,
      phone_number: phoneNumber,
      client_secret: clientSecret,
      send_attempt: sendAttempt,
      next_link: nextLink
    });
  }

  /**
   * Requests an email verification token for the purposes of adding a
   * third party identifier to an account.
   * This API requests a token from the homeserver.
   * The doesServerRequireIdServerParam() method can be used to determine if
   * the server requires the id_server parameter to be provided.
   * If an account with the given email address already exists and is
   * associated with an account other than the one the user is authed as,
   * it will either send an email to the address informing them of this
   * or return M_THREEPID_IN_USE (which one is up to the homeserver).
   *
   * @param email - As requestEmailToken
   * @param clientSecret - As requestEmailToken
   * @param sendAttempt - As requestEmailToken
   * @param nextLink - As requestEmailToken
   * @returns Promise which resolves: As requestEmailToken
   */
  requestAdd3pidEmailToken(email, clientSecret, sendAttempt, nextLink) {
    return this.requestTokenFromEndpoint("/account/3pid/email/requestToken", {
      email: email,
      client_secret: clientSecret,
      send_attempt: sendAttempt,
      next_link: nextLink
    });
  }

  /**
   * Requests a text message verification token for the purposes of adding a
   * third party identifier to an account.
   * This API proxies the identity server /validate/email/requestToken API,
   * adding specific behaviour for the addition of phone numbers to an
   * account, as requestAdd3pidEmailToken.
   *
   * @param phoneCountry - As requestRegisterMsisdnToken
   * @param phoneNumber - As requestRegisterMsisdnToken
   * @param clientSecret - As requestEmailToken
   * @param sendAttempt - As requestEmailToken
   * @param nextLink - As requestEmailToken
   * @returns Promise which resolves: As requestEmailToken
   */
  requestAdd3pidMsisdnToken(phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink) {
    return this.requestTokenFromEndpoint("/account/3pid/msisdn/requestToken", {
      country: phoneCountry,
      phone_number: phoneNumber,
      client_secret: clientSecret,
      send_attempt: sendAttempt,
      next_link: nextLink
    });
  }

  /**
   * Requests an email verification token for the purposes of resetting
   * the password on an account.
   * This API proxies the identity server /validate/email/requestToken API,
   * adding specific behaviour for the password resetting. Specifically,
   * if no account with the given email address exists, it may either
   * return M_THREEPID_NOT_FOUND or send an email
   * to the address informing them of this (which one is up to the homeserver).
   *
   * requestEmailToken calls the equivalent API directly on the identity server,
   * therefore bypassing the password reset specific logic.
   *
   * @param email - As requestEmailToken
   * @param clientSecret - As requestEmailToken
   * @param sendAttempt - As requestEmailToken
   * @param nextLink - As requestEmailToken
   * @returns Promise which resolves: As requestEmailToken
   */
  requestPasswordEmailToken(email, clientSecret, sendAttempt, nextLink) {
    return this.requestTokenFromEndpoint("/account/password/email/requestToken", {
      email: email,
      client_secret: clientSecret,
      send_attempt: sendAttempt,
      next_link: nextLink
    });
  }

  /**
   * Requests a text message verification token for the purposes of resetting
   * the password on an account.
   * This API proxies the identity server /validate/email/requestToken API,
   * adding specific behaviour for the password resetting, as requestPasswordEmailToken.
   *
   * @param phoneCountry - As requestRegisterMsisdnToken
   * @param phoneNumber - As requestRegisterMsisdnToken
   * @param clientSecret - As requestEmailToken
   * @param sendAttempt - As requestEmailToken
   * @param nextLink - As requestEmailToken
   * @returns Promise which resolves: As requestEmailToken
   */
  requestPasswordMsisdnToken(phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink) {
    return this.requestTokenFromEndpoint("/account/password/msisdn/requestToken", {
      country: phoneCountry,
      phone_number: phoneNumber,
      client_secret: clientSecret,
      send_attempt: sendAttempt,
      next_link: nextLink
    });
  }

  /**
   * Internal utility function for requesting validation tokens from usage-specific
   * requestToken endpoints.
   *
   * @param endpoint - The endpoint to send the request to
   * @param params - Parameters for the POST request
   * @returns Promise which resolves: As requestEmailToken
   */
  async requestTokenFromEndpoint(endpoint, params) {
    const postParams = Object.assign({}, params);

    // If the HS supports separate add and bind, then requestToken endpoints
    // don't need an IS as they are all validated by the HS directly.
    if (!(await this.doesServerSupportSeparateAddAndBind()) && this.idBaseUrl) {
      var _this$identityServer2;
      const idServerUrl = new URL(this.idBaseUrl);
      postParams.id_server = idServerUrl.host;
      if ((_this$identityServer2 = this.identityServer) !== null && _this$identityServer2 !== void 0 && _this$identityServer2.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) {
        const identityAccessToken = await this.identityServer.getAccessToken();
        if (identityAccessToken) {
          postParams.id_access_token = identityAccessToken;
        }
      }
    }
    return this.http.request(_httpApi.Method.Post, endpoint, undefined, postParams);
  }

  /**
   * Get the room-kind push rule associated with a room.
   * @param scope - "global" or device-specific.
   * @param roomId - the id of the room.
   * @returns the rule or undefined.
   */
  getRoomPushRule(scope, roomId) {
    // There can be only room-kind push rule per room
    // and its id is the room id.
    if (this.pushRules) {
      var _this$pushRules$scope, _this$pushRules$scope2;
      return (_this$pushRules$scope = this.pushRules[scope]) === null || _this$pushRules$scope === void 0 ? void 0 : (_this$pushRules$scope2 = _this$pushRules$scope.room) === null || _this$pushRules$scope2 === void 0 ? void 0 : _this$pushRules$scope2.find(rule => rule.rule_id === roomId);
    } else {
      throw new Error("SyncApi.sync() must be done before accessing to push rules.");
    }
  }

  /**
   * Set a room-kind muting push rule in a room.
   * The operation also updates MatrixClient.pushRules at the end.
   * @param scope - "global" or device-specific.
   * @param roomId - the id of the room.
   * @param mute - the mute state.
   * @returns Promise which resolves: result object
   * @returns Rejects: with an error response.
   */
  setRoomMutePushRule(scope, roomId, mute) {
    let promise;
    let hasDontNotifyRule = false;

    // Get the existing room-kind push rule if any
    const roomPushRule = this.getRoomPushRule(scope, roomId);
    if (roomPushRule !== null && roomPushRule !== void 0 && roomPushRule.actions.includes(_PushRules.PushRuleActionName.DontNotify)) {
      hasDontNotifyRule = true;
    }
    if (!mute) {
      // Remove the rule only if it is a muting rule
      if (hasDontNotifyRule) {
        promise = this.deletePushRule(scope, _PushRules.PushRuleKind.RoomSpecific, roomPushRule.rule_id);
      }
    } else {
      if (!roomPushRule) {
        promise = this.addPushRule(scope, _PushRules.PushRuleKind.RoomSpecific, roomId, {
          actions: [_PushRules.PushRuleActionName.DontNotify]
        });
      } else if (!hasDontNotifyRule) {
        // Remove the existing one before setting the mute push rule
        // This is a workaround to SYN-590 (Push rule update fails)
        const deferred = utils.defer();
        this.deletePushRule(scope, _PushRules.PushRuleKind.RoomSpecific, roomPushRule.rule_id).then(() => {
          this.addPushRule(scope, _PushRules.PushRuleKind.RoomSpecific, roomId, {
            actions: [_PushRules.PushRuleActionName.DontNotify]
          }).then(() => {
            deferred.resolve();
          }).catch(err => {
            deferred.reject(err);
          });
        }).catch(err => {
          deferred.reject(err);
        });
        promise = deferred.promise;
      }
    }
    if (promise) {
      return new Promise((resolve, reject) => {
        // Update this.pushRules when the operation completes
        promise.then(() => {
          this.getPushRules().then(result => {
            this.pushRules = result;
            resolve();
          }).catch(err => {
            reject(err);
          });
        }).catch(err => {
          // Update it even if the previous operation fails. This can help the
          // app to recover when push settings has been modified from another client
          this.getPushRules().then(result => {
            this.pushRules = result;
            reject(err);
          }).catch(err2 => {
            reject(err);
          });
        });
      });
    }
  }
  searchMessageText(opts) {
    const roomEvents = {
      search_term: opts.query
    };
    if ("keys" in opts) {
      roomEvents.keys = opts.keys;
    }
    return this.search({
      body: {
        search_categories: {
          room_events: roomEvents
        }
      }
    });
  }

  /**
   * Perform a server-side search for room events.
   *
   * The returned promise resolves to an object containing the fields:
   *
   *  * count:       estimate of the number of results
   *  * next_batch:  token for back-pagination; if undefined, there are no more results
   *  * highlights:  a list of words to highlight from the stemming algorithm
   *  * results:     a list of results
   *
   * Each entry in the results list is a SearchResult.
   *
   * @returns Promise which resolves: result object
   * @returns Rejects: with an error response.
   */
  searchRoomEvents(opts) {
    // TODO: support search groups

    const body = {
      search_categories: {
        room_events: {
          search_term: opts.term,
          filter: opts.filter,
          order_by: _search.SearchOrderBy.Recent,
          event_context: {
            before_limit: 1,
            after_limit: 1,
            include_profile: true
          }
        }
      }
    };
    const searchResults = {
      _query: body,
      results: [],
      highlights: []
    };
    return this.search({
      body: body
    }).then(res => this.processRoomEventsSearch(searchResults, res));
  }

  /**
   * Take a result from an earlier searchRoomEvents call, and backfill results.
   *
   * @param searchResults -  the results object to be updated
   * @returns Promise which resolves: updated result object
   * @returns Rejects: with an error response.
   */
  backPaginateRoomEventsSearch(searchResults) {
    // TODO: we should implement a backoff (as per scrollback()) to deal more
    // nicely with HTTP errors.

    if (!searchResults.next_batch) {
      return Promise.reject(new Error("Cannot backpaginate event search any further"));
    }
    if (searchResults.pendingRequest) {
      // already a request in progress - return the existing promise
      return searchResults.pendingRequest;
    }
    const searchOpts = {
      body: searchResults._query,
      next_batch: searchResults.next_batch
    };
    const promise = this.search(searchOpts, searchResults.abortSignal).then(res => this.processRoomEventsSearch(searchResults, res)).finally(() => {
      searchResults.pendingRequest = undefined;
    });
    searchResults.pendingRequest = promise;
    return promise;
  }

  /**
   * helper for searchRoomEvents and backPaginateRoomEventsSearch. Processes the
   * response from the API call and updates the searchResults
   *
   * @returns searchResults
   * @internal
   */
  // XXX: Intended private, used in code
  processRoomEventsSearch(searchResults, response) {
    var _roomEvents$results$l, _roomEvents$results;
    const roomEvents = response.search_categories.room_events;
    searchResults.count = roomEvents.count;
    searchResults.next_batch = roomEvents.next_batch;

    // combine the highlight list with our existing list;
    const highlights = new Set(roomEvents.highlights);
    searchResults.highlights.forEach(hl => {
      highlights.add(hl);
    });

    // turn it back into a list.
    searchResults.highlights = Array.from(highlights);
    const mapper = this.getEventMapper();

    // append the new results to our existing results
    const resultsLength = (_roomEvents$results$l = (_roomEvents$results = roomEvents.results) === null || _roomEvents$results === void 0 ? void 0 : _roomEvents$results.length) !== null && _roomEvents$results$l !== void 0 ? _roomEvents$results$l : 0;
    for (let i = 0; i < resultsLength; i++) {
      const sr = _searchResult.SearchResult.fromJson(roomEvents.results[i], mapper);
      const room = this.getRoom(sr.context.getEvent().getRoomId());
      if (room) {
        // Copy over a known event sender if we can
        for (const ev of sr.context.getTimeline()) {
          const sender = room.getMember(ev.getSender());
          if (!ev.sender && sender) ev.sender = sender;
        }
      }
      searchResults.results.push(sr);
    }
    return searchResults;
  }

  /**
   * Populate the store with rooms the user has left.
   * @returns Promise which resolves: TODO - Resolved when the rooms have
   * been added to the data store.
   * @returns Rejects: with an error response.
   */
  syncLeftRooms() {
    // Guard against multiple calls whilst ongoing and multiple calls post success
    if (this.syncedLeftRooms) {
      return Promise.resolve([]); // don't call syncRooms again if it succeeded.
    }

    if (this.syncLeftRoomsPromise) {
      return this.syncLeftRoomsPromise; // return the ongoing request
    }

    const syncApi = new _sync.SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
    this.syncLeftRoomsPromise = syncApi.syncLeftRooms();

    // cleanup locks
    this.syncLeftRoomsPromise.then(() => {
      _logger.logger.log("Marking success of sync left room request");
      this.syncedLeftRooms = true; // flip the bit on success
    }).finally(() => {
      this.syncLeftRoomsPromise = undefined; // cleanup ongoing request state
    });

    return this.syncLeftRoomsPromise;
  }

  /**
   * Create a new filter.
   * @param content - The HTTP body for the request
   * @returns Promise which resolves to a Filter object.
   * @returns Rejects: with an error response.
   */
  createFilter(content) {
    const path = utils.encodeUri("/user/$userId/filter", {
      $userId: this.credentials.userId
    });
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, content).then(response => {
      // persist the filter
      const filter = _filter.Filter.fromJson(this.credentials.userId, response.filter_id, content);
      this.store.storeFilter(filter);
      return filter;
    });
  }

  /**
   * Retrieve a filter.
   * @param userId - The user ID of the filter owner
   * @param filterId - The filter ID to retrieve
   * @param allowCached - True to allow cached filters to be returned.
   * Default: True.
   * @returns Promise which resolves: a Filter object
   * @returns Rejects: with an error response.
   */
  getFilter(userId, filterId, allowCached) {
    if (allowCached) {
      const filter = this.store.getFilter(userId, filterId);
      if (filter) {
        return Promise.resolve(filter);
      }
    }
    const path = utils.encodeUri("/user/$userId/filter/$filterId", {
      $userId: userId,
      $filterId: filterId
    });
    return this.http.authedRequest(_httpApi.Method.Get, path).then(response => {
      // persist the filter
      const filter = _filter.Filter.fromJson(userId, filterId, response);
      this.store.storeFilter(filter);
      return filter;
    });
  }

  /**
   * @returns Filter ID
   */
  async getOrCreateFilter(filterName, filter) {
    const filterId = this.store.getFilterIdByName(filterName);
    let existingId;
    if (filterId) {
      // check that the existing filter matches our expectations
      try {
        const existingFilter = await this.getFilter(this.credentials.userId, filterId, true);
        if (existingFilter) {
          const oldDef = existingFilter.getDefinition();
          const newDef = filter.getDefinition();
          if (utils.deepCompare(oldDef, newDef)) {
            // super, just use that.
            // debuglog("Using existing filter ID %s: %s", filterId,
            //          JSON.stringify(oldDef));
            existingId = filterId;
          }
        }
      } catch (error) {
        // Synapse currently returns the following when the filter cannot be found:
        // {
        //     errcode: "M_UNKNOWN",
        //     name: "M_UNKNOWN",
        //     message: "No row found",
        // }
        if (error.errcode !== "M_UNKNOWN" && error.errcode !== "M_NOT_FOUND") {
          throw error;
        }
      }
      // if the filter doesn't exist anymore on the server, remove from store
      if (!existingId) {
        this.store.setFilterIdByName(filterName, undefined);
      }
    }
    if (existingId) {
      return existingId;
    }

    // create a new filter
    const createdFilter = await this.createFilter(filter.getDefinition());
    this.store.setFilterIdByName(filterName, createdFilter.filterId);
    return createdFilter.filterId;
  }

  /**
   * Gets a bearer token from the homeserver that the user can
   * present to a third party in order to prove their ownership
   * of the Matrix account they are logged into.
   * @returns Promise which resolves: Token object
   * @returns Rejects: with an error response.
   */
  getOpenIdToken() {
    const path = utils.encodeUri("/user/$userId/openid/request_token", {
      $userId: this.credentials.userId
    });
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, {});
  }
  /**
   * @returns Promise which resolves: ITurnServerResponse object
   * @returns Rejects: with an error response.
   */
  turnServer() {
    return this.http.authedRequest(_httpApi.Method.Get, "/voip/turnServer");
  }

  /**
   * Get the TURN servers for this homeserver.
   * @returns The servers or an empty list.
   */
  getTurnServers() {
    return this.turnServers || [];
  }

  /**
   * Get the unix timestamp (in milliseconds) at which the current
   * TURN credentials (from getTurnServers) expire
   * @returns The expiry timestamp in milliseconds
   */
  getTurnServersExpiry() {
    return this.turnServersExpiry;
  }
  get pollingTurnServers() {
    return this.checkTurnServersIntervalID !== undefined;
  }

  // XXX: Intended private, used in code.
  async checkTurnServers() {
    if (!this.canSupportVoip) {
      return;
    }
    let credentialsGood = false;
    const remainingTime = this.turnServersExpiry - Date.now();
    if (remainingTime > TURN_CHECK_INTERVAL) {
      _logger.logger.debug("TURN creds are valid for another " + remainingTime + " ms: not fetching new ones.");
      credentialsGood = true;
    } else {
      _logger.logger.debug("Fetching new TURN credentials");
      try {
        const res = await this.turnServer();
        if (res.uris) {
          _logger.logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs");
          // map the response to a format that can be fed to RTCPeerConnection
          const servers = {
            urls: res.uris,
            username: res.username,
            credential: res.password
          };
          this.turnServers = [servers];
          // The TTL is in seconds but we work in ms
          this.turnServersExpiry = Date.now() + res.ttl * 1000;
          credentialsGood = true;
          this.emit(ClientEvent.TurnServers, this.turnServers);
        }
      } catch (err) {
        _logger.logger.error("Failed to get TURN URIs", err);
        if (err.httpStatus === 403) {
          // We got a 403, so there's no point in looping forever.
          _logger.logger.info("TURN access unavailable for this account: stopping credentials checks");
          if (this.checkTurnServersIntervalID !== null) global.clearInterval(this.checkTurnServersIntervalID);
          this.checkTurnServersIntervalID = undefined;
          this.emit(ClientEvent.TurnServersError, err, true); // fatal
        } else {
          // otherwise, if we failed for whatever reason, try again the next time we're called.
          this.emit(ClientEvent.TurnServersError, err, false); // non-fatal
        }
      }
    }

    return credentialsGood;
  }

  /**
   * Set whether to allow a fallback ICE server should be used for negotiating a
   * WebRTC connection if the homeserver doesn't provide any servers. Defaults to
   * false.
   *
   */
  setFallbackICEServerAllowed(allow) {
    this.fallbackICEServerAllowed = allow;
  }

  /**
   * Get whether to allow a fallback ICE server should be used for negotiating a
   * WebRTC connection if the homeserver doesn't provide any servers. Defaults to
   * false.
   *
   * @returns
   */
  isFallbackICEServerAllowed() {
    return this.fallbackICEServerAllowed;
  }

  /**
   * Determines if the current user is an administrator of the Synapse homeserver.
   * Returns false if untrue or the homeserver does not appear to be a Synapse
   * homeserver. <strong>This function is implementation specific and may change
   * as a result.</strong>
   * @returns true if the user appears to be a Synapse administrator.
   */
  isSynapseAdministrator() {
    const path = utils.encodeUri("/_synapse/admin/v1/users/$userId/admin", {
      $userId: this.getUserId()
    });
    return this.http.authedRequest(_httpApi.Method.Get, path, undefined, undefined, {
      prefix: ""
    }).then(r => r.admin); // pull out the specific boolean we want
  }

  /**
   * Performs a whois lookup on a user using Synapse's administrator API.
   * <strong>This function is implementation specific and may change as a
   * result.</strong>
   * @param userId - the User ID to look up.
   * @returns the whois response - see Synapse docs for information.
   */
  whoisSynapseUser(userId) {
    const path = utils.encodeUri("/_synapse/admin/v1/whois/$userId", {
      $userId: userId
    });
    return this.http.authedRequest(_httpApi.Method.Get, path, undefined, undefined, {
      prefix: ""
    });
  }

  /**
   * Deactivates a user using Synapse's administrator API. <strong>This
   * function is implementation specific and may change as a result.</strong>
   * @param userId - the User ID to deactivate.
   * @returns the deactivate response - see Synapse docs for information.
   */
  deactivateSynapseUser(userId) {
    const path = utils.encodeUri("/_synapse/admin/v1/deactivate/$userId", {
      $userId: userId
    });
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, undefined, {
      prefix: ""
    });
  }
  async fetchClientWellKnown() {
    var _this$getDomain;
    // `getRawClientConfig` does not throw or reject on network errors, instead
    // it absorbs errors and returns `{}`.
    this.clientWellKnownPromise = _autodiscovery.AutoDiscovery.getRawClientConfig((_this$getDomain = this.getDomain()) !== null && _this$getDomain !== void 0 ? _this$getDomain : undefined);
    this.clientWellKnown = await this.clientWellKnownPromise;
    this.emit(ClientEvent.ClientWellKnown, this.clientWellKnown);
  }
  getClientWellKnown() {
    return this.clientWellKnown;
  }
  waitForClientWellKnown() {
    if (!this.clientRunning) {
      throw new Error("Client is not running");
    }
    return this.clientWellKnownPromise;
  }

  /**
   * store client options with boolean/string/numeric values
   * to know in the next session what flags the sync data was
   * created with (e.g. lazy loading)
   * @param opts - the complete set of client options
   * @returns for store operation
   */
  storeClientOptions() {
    // XXX: Intended private, used in code
    const primTypes = ["boolean", "string", "number"];
    const serializableOpts = Object.entries(this.clientOpts).filter(([key, value]) => {
      return primTypes.includes(typeof value);
    }).reduce((obj, [key, value]) => {
      obj[key] = value;
      return obj;
    }, {});
    return this.store.storeClientOptions(serializableOpts);
  }

  /**
   * Gets a set of room IDs in common with another user
   * @param userId - The userId to check.
   * @returns Promise which resolves to a set of rooms
   * @returns Rejects: with an error response.
   */
  // eslint-disable-next-line
  async _unstable_getSharedRooms(userId) {
    const sharedRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666");
    const mutualRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666.mutual_rooms");
    if (!sharedRoomsSupport && !mutualRoomsSupport) {
      throw Error("Server does not support mutual_rooms API");
    }
    const path = utils.encodeUri(`/uk.half-shot.msc2666/user/${mutualRoomsSupport ? "mutual_rooms" : "shared_rooms"}/$userId`, {
      $userId: userId
    });
    const res = await this.http.authedRequest(_httpApi.Method.Get, path, undefined, undefined, {
      prefix: _httpApi.ClientPrefix.Unstable
    });
    return res.joined;
  }

  /**
   * Get the API versions supported by the server, along with any
   * unstable APIs it supports
   * @returns The server /versions response
   */
  async getVersions() {
    if (this.serverVersionsPromise) {
      return this.serverVersionsPromise;
    }
    this.serverVersionsPromise = this.http.request(_httpApi.Method.Get, "/_matrix/client/versions", undefined,
    // queryParams
    undefined,
    // data
    {
      prefix: ""
    }).catch(e => {
      // Need to unset this if it fails, otherwise we'll never retry
      this.serverVersionsPromise = undefined;
      // but rethrow the exception to anything that was waiting
      throw e;
    });
    const serverVersions = await this.serverVersionsPromise;
    this.canSupport = await (0, _feature.buildFeatureSupportMap)(serverVersions);
    return this.serverVersionsPromise;
  }

  /**
   * Check if a particular spec version is supported by the server.
   * @param version - The spec version (such as "r0.5.0") to check for.
   * @returns Whether it is supported
   */
  async isVersionSupported(version) {
    const {
      versions
    } = await this.getVersions();
    return versions && versions.includes(version);
  }

  /**
   * Query the server to see if it supports members lazy loading
   * @returns true if server supports lazy loading
   */
  async doesServerSupportLazyLoading() {
    const response = await this.getVersions();
    if (!response) return false;
    const versions = response["versions"];
    const unstableFeatures = response["unstable_features"];
    return versions && versions.includes("r0.5.0") || unstableFeatures && unstableFeatures["m.lazy_load_members"];
  }

  /**
   * Query the server to see if the `id_server` parameter is required
   * when registering with an 3pid, adding a 3pid or resetting password.
   * @returns true if id_server parameter is required
   */
  async doesServerRequireIdServerParam() {
    const response = await this.getVersions();
    if (!response) return true;
    const versions = response["versions"];

    // Supporting r0.6.0 is the same as having the flag set to false
    if (versions && versions.includes("r0.6.0")) {
      return false;
    }
    const unstableFeatures = response["unstable_features"];
    if (!unstableFeatures) return true;
    if (unstableFeatures["m.require_identity_server"] === undefined) {
      return true;
    } else {
      return unstableFeatures["m.require_identity_server"];
    }
  }

  /**
   * Query the server to see if the `id_access_token` parameter can be safely
   * passed to the homeserver. Some homeservers may trigger errors if they are not
   * prepared for the new parameter.
   * @returns true if id_access_token can be sent
   */
  async doesServerAcceptIdentityAccessToken() {
    const response = await this.getVersions();
    if (!response) return false;
    const versions = response["versions"];
    const unstableFeatures = response["unstable_features"];
    return versions && versions.includes("r0.6.0") || unstableFeatures && unstableFeatures["m.id_access_token"];
  }

  /**
   * Query the server to see if it supports separate 3PID add and bind functions.
   * This affects the sequence of API calls clients should use for these operations,
   * so it's helpful to be able to check for support.
   * @returns true if separate functions are supported
   */
  async doesServerSupportSeparateAddAndBind() {
    const response = await this.getVersions();
    if (!response) return false;
    const versions = response["versions"];
    const unstableFeatures = response["unstable_features"];
    return (versions === null || versions === void 0 ? void 0 : versions.includes("r0.6.0")) || (unstableFeatures === null || unstableFeatures === void 0 ? void 0 : unstableFeatures["m.separate_add_and_bind"]);
  }

  /**
   * Query the server to see if it lists support for an unstable feature
   * in the /versions response
   * @param feature - the feature name
   * @returns true if the feature is supported
   */
  async doesServerSupportUnstableFeature(feature) {
    const response = await this.getVersions();
    if (!response) return false;
    const unstableFeatures = response["unstable_features"];
    return unstableFeatures && !!unstableFeatures[feature];
  }

  /**
   * Query the server to see if it is forcing encryption to be enabled for
   * a given room preset, based on the /versions response.
   * @param presetName - The name of the preset to check.
   * @returns true if the server is forcing encryption
   * for the preset.
   */
  async doesServerForceEncryptionForPreset(presetName) {
    const response = await this.getVersions();
    if (!response) return false;
    const unstableFeatures = response["unstable_features"];

    // The preset name in the versions response will be without the _chat suffix.
    const versionsPresetName = presetName.includes("_chat") ? presetName.substring(0, presetName.indexOf("_chat")) : presetName;
    return unstableFeatures && !!unstableFeatures[`io.element.e2ee_forced.${versionsPresetName}`];
  }
  async doesServerSupportThread() {
    if (await this.isVersionSupported("v1.4")) {
      return {
        threads: _thread.FeatureSupport.Stable,
        list: _thread.FeatureSupport.Stable,
        fwdPagination: _thread.FeatureSupport.Stable
      };
    }
    try {
      const [threadUnstable, threadStable, listUnstable, listStable, fwdPaginationUnstable, fwdPaginationStable] = await Promise.all([this.doesServerSupportUnstableFeature("org.matrix.msc3440"), this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"), this.doesServerSupportUnstableFeature("org.matrix.msc3856"), this.doesServerSupportUnstableFeature("org.matrix.msc3856.stable"), this.doesServerSupportUnstableFeature("org.matrix.msc3715"), this.doesServerSupportUnstableFeature("org.matrix.msc3715.stable")]);
      return {
        threads: (0, _thread.determineFeatureSupport)(threadStable, threadUnstable),
        list: (0, _thread.determineFeatureSupport)(listStable, listUnstable),
        fwdPagination: (0, _thread.determineFeatureSupport)(fwdPaginationStable, fwdPaginationUnstable)
      };
    } catch (e) {
      return {
        threads: _thread.FeatureSupport.None,
        list: _thread.FeatureSupport.None,
        fwdPagination: _thread.FeatureSupport.None
      };
    }
  }

  /**
   * Query the server to see if it supports the MSC2457 `logout_devices` parameter when setting password
   * @returns true if server supports the `logout_devices` parameter
   */
  doesServerSupportLogoutDevices() {
    return this.isVersionSupported("r0.6.1");
  }

  /**
   * Get if lazy loading members is being used.
   * @returns Whether or not members are lazy loaded by this client
   */
  hasLazyLoadMembersEnabled() {
    var _this$clientOpts6;
    return !!((_this$clientOpts6 = this.clientOpts) !== null && _this$clientOpts6 !== void 0 && _this$clientOpts6.lazyLoadMembers);
  }

  /**
   * Set a function which is called when /sync returns a 'limited' response.
   * It is called with a room ID and returns a boolean. It should return 'true' if the SDK
   * can SAFELY remove events from this room. It may not be safe to remove events if there
   * are other references to the timelines for this room, e.g because the client is
   * actively viewing events in this room.
   * Default: returns false.
   * @param cb - The callback which will be invoked.
   */
  setCanResetTimelineCallback(cb) {
    this.canResetTimelineCallback = cb;
  }

  /**
   * Get the callback set via `setCanResetTimelineCallback`.
   * @returns The callback or null
   */
  getCanResetTimelineCallback() {
    return this.canResetTimelineCallback;
  }

  /**
   * Returns relations for a given event. Handles encryption transparently,
   * with the caveat that the amount of events returned might be 0, even though you get a nextBatch.
   * When the returned promise resolves, all messages should have finished trying to decrypt.
   * @param roomId - the room of the event
   * @param eventId - the id of the event
   * @param relationType - the rel_type of the relations requested
   * @param eventType - the event type of the relations requested
   * @param opts - options with optional values for the request.
   * @returns an object with `events` as `MatrixEvent[]` and optionally `nextBatch` if more relations are available.
   */
  async relations(roomId, eventId, relationType, eventType, opts = {
    dir: _eventTimeline.Direction.Backward
  }) {
    var _result$next_batch, _result$prev_batch;
    const fetchedEventType = eventType ? this.getEncryptedIfNeededEventType(roomId, eventType) : null;
    const [eventResult, result] = await Promise.all([this.fetchRoomEvent(roomId, eventId), this.fetchRelations(roomId, eventId, relationType, fetchedEventType, opts)]);
    const mapper = this.getEventMapper();
    const originalEvent = eventResult ? mapper(eventResult) : undefined;
    let events = result.chunk.map(mapper);
    if (fetchedEventType === _event2.EventType.RoomMessageEncrypted) {
      const allEvents = originalEvent ? events.concat(originalEvent) : events;
      await Promise.all(allEvents.map(e => this.decryptEventIfNeeded(e)));
      if (eventType !== null) {
        events = events.filter(e => e.getType() === eventType);
      }
    }
    if (originalEvent && relationType === _event2.RelationType.Replace) {
      events = events.filter(e => e.getSender() === originalEvent.getSender());
    }
    return {
      originalEvent: originalEvent !== null && originalEvent !== void 0 ? originalEvent : null,
      events,
      nextBatch: (_result$next_batch = result.next_batch) !== null && _result$next_batch !== void 0 ? _result$next_batch : null,
      prevBatch: (_result$prev_batch = result.prev_batch) !== null && _result$prev_batch !== void 0 ? _result$prev_batch : null
    };
  }

  /**
   * The app may wish to see if we have a key cached without
   * triggering a user interaction.
   */
  getCrossSigningCacheCallbacks() {
    var _this$crypto3;
    // XXX: Private member access
    return (_this$crypto3 = this.crypto) === null || _this$crypto3 === void 0 ? void 0 : _this$crypto3.crossSigningInfo.getCacheCallbacks();
  }

  /**
   * Generates a random string suitable for use as a client secret. <strong>This
   * method is experimental and may change.</strong>
   * @returns A new client secret
   */
  generateClientSecret() {
    return (0, _randomstring.randomString)(32);
  }

  /**
   * Attempts to decrypt an event
   * @param event - The event to decrypt
   * @returns A decryption promise
   */
  decryptEventIfNeeded(event, options) {
    if (event.shouldAttemptDecryption() && this.isCryptoEnabled()) {
      event.attemptDecryption(this.cryptoBackend, options);
    }
    if (event.isBeingDecrypted()) {
      return event.getDecryptionPromise();
    } else {
      return Promise.resolve();
    }
  }
  termsUrlForService(serviceType, baseUrl) {
    switch (serviceType) {
      case _serviceTypes.SERVICE_TYPES.IS:
        return this.http.getUrl("/terms", undefined, _httpApi.IdentityPrefix.V2, baseUrl);
      case _serviceTypes.SERVICE_TYPES.IM:
        return this.http.getUrl("/terms", undefined, "/_matrix/integrations/v1", baseUrl);
      default:
        throw new Error("Unsupported service type");
    }
  }

  /**
   * Get the Homeserver URL of this client
   * @returns Homeserver URL of this client
   */
  getHomeserverUrl() {
    return this.baseUrl;
  }

  /**
   * Get the identity server URL of this client
   * @param stripProto - whether or not to strip the protocol from the URL
   * @returns Identity server URL of this client
   */
  getIdentityServerUrl(stripProto = false) {
    var _this$idBaseUrl, _this$idBaseUrl2;
    if (stripProto && ((_this$idBaseUrl = this.idBaseUrl) !== null && _this$idBaseUrl !== void 0 && _this$idBaseUrl.startsWith("http://") || (_this$idBaseUrl2 = this.idBaseUrl) !== null && _this$idBaseUrl2 !== void 0 && _this$idBaseUrl2.startsWith("https://"))) {
      return this.idBaseUrl.split("://")[1];
    }
    return this.idBaseUrl;
  }

  /**
   * Set the identity server URL of this client
   * @param url - New identity server URL
   */
  setIdentityServerUrl(url) {
    this.idBaseUrl = utils.ensureNoTrailingSlash(url);
    this.http.setIdBaseUrl(this.idBaseUrl);
  }

  /**
   * Get the access token associated with this account.
   * @returns The access_token or null
   */
  getAccessToken() {
    return this.http.opts.accessToken || null;
  }

  /**
   * Set the access token associated with this account.
   * @param token - The new access token.
   */
  setAccessToken(token) {
    this.http.opts.accessToken = token;
  }

  /**
   * @returns true if there is a valid access_token for this client.
   */
  isLoggedIn() {
    return this.http.opts.accessToken !== undefined;
  }

  /**
   * Make up a new transaction id
   *
   * @returns a new, unique, transaction id
   */
  makeTxnId() {
    return "m" + new Date().getTime() + "." + this.txnCtr++;
  }

  /**
   * Check whether a username is available prior to registration. An error response
   * indicates an invalid/unavailable username.
   * @param username - The username to check the availability of.
   * @returns Promise which resolves: to boolean of whether the username is available.
   */
  isUsernameAvailable(username) {
    return this.http.authedRequest(_httpApi.Method.Get, "/register/available", {
      username
    }).then(response => {
      return response.available;
    }).catch(response => {
      if (response.errcode === "M_USER_IN_USE") {
        return false;
      }
      return Promise.reject(response);
    });
  }

  /**
   * @param bindThreepids - Set key 'email' to true to bind any email
   *     threepid uses during registration in the identity server. Set 'msisdn' to
   *     true to bind msisdn.
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  register(username, password, sessionId, auth, bindThreepids, guestAccessToken, inhibitLogin) {
    // backwards compat
    if (bindThreepids === true) {
      bindThreepids = {
        email: true
      };
    } else if (bindThreepids === null || bindThreepids === undefined || bindThreepids === false) {
      bindThreepids = {};
    }
    if (sessionId) {
      auth.session = sessionId;
    }
    const params = {
      auth: auth,
      refresh_token: true // always ask for a refresh token - does nothing if unsupported
    };

    if (username !== undefined && username !== null) {
      params.username = username;
    }
    if (password !== undefined && password !== null) {
      params.password = password;
    }
    if (bindThreepids.email) {
      params.bind_email = true;
    }
    if (bindThreepids.msisdn) {
      params.bind_msisdn = true;
    }
    if (guestAccessToken !== undefined && guestAccessToken !== null) {
      params.guest_access_token = guestAccessToken;
    }
    if (inhibitLogin !== undefined && inhibitLogin !== null) {
      params.inhibit_login = inhibitLogin;
    }
    // Temporary parameter added to make the register endpoint advertise
    // msisdn flows. This exists because there are clients that break
    // when given stages they don't recognise. This parameter will cease
    // to be necessary once these old clients are gone.
    // Only send it if we send any params at all (the password param is
    // mandatory, so if we send any params, we'll send the password param)
    if (password !== undefined && password !== null) {
      params.x_show_msisdn = true;
    }
    return this.registerRequest(params);
  }

  /**
   * Register a guest account.
   * This method returns the auth info needed to create a new authenticated client,
   * Remember to call `setGuest(true)` on the (guest-)authenticated client, e.g:
   * ```javascript
   * const tmpClient = await sdk.createClient(MATRIX_INSTANCE);
   * const { user_id, device_id, access_token } = tmpClient.registerGuest();
   * const client = createClient({
   *   baseUrl: MATRIX_INSTANCE,
   *   accessToken: access_token,
   *   userId: user_id,
   *   deviceId: device_id,
   * })
   * client.setGuest(true);
   * ```
   *
   * @param body - JSON HTTP body to provide.
   * @returns Promise which resolves: JSON object that contains:
   *                   `{ user_id, device_id, access_token, home_server }`
   * @returns Rejects: with an error response.
   */
  registerGuest({
    body
  } = {}) {
    // TODO: Types
    return this.registerRequest(body || {}, "guest");
  }

  /**
   * @param data - parameters for registration request
   * @param kind - type of user to register. may be "guest"
   * @returns Promise which resolves: to the /register response
   * @returns Rejects: with an error response.
   */
  registerRequest(data, kind) {
    const params = {};
    if (kind) {
      params.kind = kind;
    }
    return this.http.request(_httpApi.Method.Post, "/register", params, data);
  }

  /**
   * Refreshes an access token using a provided refresh token. The refresh token
   * must be valid for the current access token known to the client instance.
   *
   * Note that this function will not cause a logout if the token is deemed
   * unknown by the server - the caller is responsible for managing logout
   * actions on error.
   * @param refreshToken - The refresh token.
   * @returns Promise which resolves to the new token.
   * @returns Rejects with an error response.
   */
  refreshToken(refreshToken) {
    return this.http.authedRequest(_httpApi.Method.Post, "/refresh", undefined, {
      refresh_token: refreshToken
    }, {
      prefix: _httpApi.ClientPrefix.V1,
      inhibitLogoutEmit: true // we don't want to cause logout loops
    });
  }

  /**
   * @returns Promise which resolves to the available login flows
   * @returns Rejects: with an error response.
   */
  loginFlows() {
    return this.http.request(_httpApi.Method.Get, "/login");
  }

  /**
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  login(loginType, data) {
    // TODO: Types
    const loginData = {
      type: loginType
    };

    // merge data into loginData
    Object.assign(loginData, data);
    return this.http.authedRequest(_httpApi.Method.Post, "/login", undefined, loginData).then(response => {
      if (response.access_token && response.user_id) {
        this.http.opts.accessToken = response.access_token;
        this.credentials = {
          userId: response.user_id
        };
      }
      return response;
    });
  }

  /**
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  loginWithPassword(user, password) {
    // TODO: Types
    return this.login("m.login.password", {
      user: user,
      password: password
    });
  }

  /**
   * @param relayState - URL Callback after SAML2 Authentication
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  loginWithSAML2(relayState) {
    // TODO: Types
    return this.login("m.login.saml2", {
      relay_state: relayState
    });
  }

  /**
   * @param redirectUrl - The URL to redirect to after the HS
   * authenticates with CAS.
   * @returns The HS URL to hit to begin the CAS login process.
   */
  getCasLoginUrl(redirectUrl) {
    return this.getSsoLoginUrl(redirectUrl, "cas");
  }

  /**
   * @param redirectUrl - The URL to redirect to after the HS
   *     authenticates with the SSO.
   * @param loginType - The type of SSO login we are doing (sso or cas).
   *     Defaults to 'sso'.
   * @param idpId - The ID of the Identity Provider being targeted, optional.
   * @param action - the SSO flow to indicate to the IdP, optional.
   * @returns The HS URL to hit to begin the SSO login process.
   */
  getSsoLoginUrl(redirectUrl, loginType = "sso", idpId, action) {
    let url = "/login/" + loginType + "/redirect";
    if (idpId) {
      url += "/" + idpId;
    }
    const params = {
      redirectUrl,
      [SSO_ACTION_PARAM.unstable]: action
    };
    return this.http.getUrl(url, params, _httpApi.ClientPrefix.R0).href;
  }

  /**
   * @param token - Login token previously received from homeserver
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  loginWithToken(token) {
    // TODO: Types
    return this.login("m.login.token", {
      token: token
    });
  }

  /**
   * Logs out the current session.
   * Obviously, further calls that require authorisation should fail after this
   * method is called. The state of the MatrixClient object is not affected:
   * it is up to the caller to either reset or destroy the MatrixClient after
   * this method succeeds.
   * @param stopClient - whether to stop the client before calling /logout to prevent invalid token errors.
   * @returns Promise which resolves: On success, the empty object `{}`
   */
  async logout(stopClient = false) {
    var _this$crypto4, _this$crypto4$backupM;
    if ((_this$crypto4 = this.crypto) !== null && _this$crypto4 !== void 0 && (_this$crypto4$backupM = _this$crypto4.backupManager) !== null && _this$crypto4$backupM !== void 0 && _this$crypto4$backupM.getKeyBackupEnabled()) {
      try {
        while ((await this.crypto.backupManager.backupPendingKeys(200)) > 0);
      } catch (err) {
        _logger.logger.error("Key backup request failed when logging out. Some keys may be missing from backup", err);
      }
    }
    if (stopClient) {
      this.stopClient();
      this.http.abort();
    }
    return this.http.authedRequest(_httpApi.Method.Post, "/logout");
  }

  /**
   * Deactivates the logged-in account.
   * Obviously, further calls that require authorisation should fail after this
   * method is called. The state of the MatrixClient object is not affected:
   * it is up to the caller to either reset or destroy the MatrixClient after
   * this method succeeds.
   * @param auth - Optional. Auth data to supply for User-Interactive auth.
   * @param erase - Optional. If set, send as `erase` attribute in the
   * JSON request body, indicating whether the account should be erased. Defaults
   * to false.
   * @returns Promise which resolves: On success, the empty object
   */
  deactivateAccount(auth, erase) {
    const body = {};
    if (auth) {
      body.auth = auth;
    }
    if (erase !== undefined) {
      body.erase = erase;
    }
    return this.http.authedRequest(_httpApi.Method.Post, "/account/deactivate", undefined, body);
  }

  /**
   * Make a request for an `m.login.token` to be issued as per
   * [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882).
   * The server may require User-Interactive auth.
   * Note that this is UNSTABLE and subject to breaking changes without notice.
   * @param auth - Optional. Auth data to supply for User-Interactive auth.
   * @returns Promise which resolves: On success, the token response
   * or UIA auth data.
   */
  requestLoginToken(auth) {
    const body = {
      auth
    };
    return this.http.authedRequest(_httpApi.Method.Post, "/org.matrix.msc3882/login/token", undefined,
    // no query params
    body, {
      prefix: _httpApi.ClientPrefix.Unstable
    });
  }

  /**
   * Get the fallback URL to use for unknown interactive-auth stages.
   *
   * @param loginType -     the type of stage being attempted
   * @param authSessionId - the auth session ID provided by the homeserver
   *
   * @returns HS URL to hit to for the fallback interface
   */
  getFallbackAuthUrl(loginType, authSessionId) {
    const path = utils.encodeUri("/auth/$loginType/fallback/web", {
      $loginType: loginType
    });
    return this.http.getUrl(path, {
      session: authSessionId
    }, _httpApi.ClientPrefix.R0).href;
  }

  /**
   * Create a new room.
   * @param options - a list of options to pass to the /createRoom API.
   * @returns Promise which resolves: `{room_id: {string}}`
   * @returns Rejects: with an error response.
   */
  async createRoom(options) {
    var _this$identityServer3;
    // eslint-disable-line camelcase
    // some valid options include: room_alias_name, visibility, invite

    // inject the id_access_token if inviting 3rd party addresses
    const invitesNeedingToken = (options.invite_3pid || []).filter(i => !i.id_access_token);
    if (invitesNeedingToken.length > 0 && (_this$identityServer3 = this.identityServer) !== null && _this$identityServer3 !== void 0 && _this$identityServer3.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) {
      const identityAccessToken = await this.identityServer.getAccessToken();
      if (identityAccessToken) {
        for (const invite of invitesNeedingToken) {
          invite.id_access_token = identityAccessToken;
        }
      }
    }
    return this.http.authedRequest(_httpApi.Method.Post, "/createRoom", undefined, options);
  }

  /**
   * Fetches relations for a given event
   * @param roomId - the room of the event
   * @param eventId - the id of the event
   * @param relationType - the rel_type of the relations requested
   * @param eventType - the event type of the relations requested
   * @param opts - options with optional values for the request.
   * @returns the response, with chunk, prev_batch and, next_batch.
   */
  fetchRelations(roomId, eventId, relationType, eventType, opts = {
    dir: _eventTimeline.Direction.Backward
  }) {
    let params = opts;
    if (_thread.Thread.hasServerSideFwdPaginationSupport === _thread.FeatureSupport.Experimental) {
      params = (0, utils.replaceParam)("dir", "org.matrix.msc3715.dir", params);
    }
    const queryString = utils.encodeParams(params);
    let templatedUrl = "/rooms/$roomId/relations/$eventId";
    if (relationType !== null) {
      templatedUrl += "/$relationType";
      if (eventType !== null) {
        templatedUrl += "/$eventType";
      }
    } else if (eventType !== null) {
      _logger.logger.warn(`eventType: ${eventType} ignored when fetching
            relations as relationType is null`);
      eventType = null;
    }
    const path = utils.encodeUri(templatedUrl + "?" + queryString, {
      $roomId: roomId,
      $eventId: eventId,
      $relationType: relationType,
      $eventType: eventType
    });
    return this.http.authedRequest(_httpApi.Method.Get, path, undefined, undefined, {
      prefix: _httpApi.ClientPrefix.V1
    });
  }

  /**
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  roomState(roomId) {
    const path = utils.encodeUri("/rooms/$roomId/state", {
      $roomId: roomId
    });
    return this.http.authedRequest(_httpApi.Method.Get, path);
  }

  /**
   * Get an event in a room by its event id.
   *
   * @returns Promise which resolves to an object containing the event.
   * @returns Rejects: with an error response.
   */
  fetchRoomEvent(roomId, eventId) {
    const path = utils.encodeUri("/rooms/$roomId/event/$eventId", {
      $roomId: roomId,
      $eventId: eventId
    });
    return this.http.authedRequest(_httpApi.Method.Get, path);
  }

  /**
   * @param includeMembership - the membership type to include in the response
   * @param excludeMembership - the membership type to exclude from the response
   * @param atEventId - the id of the event for which moment in the timeline the members should be returned for
   * @returns Promise which resolves: dictionary of userid to profile information
   * @returns Rejects: with an error response.
   */
  members(roomId, includeMembership, excludeMembership, atEventId) {
    const queryParams = {};
    if (includeMembership) {
      queryParams.membership = includeMembership;
    }
    if (excludeMembership) {
      queryParams.not_membership = excludeMembership;
    }
    if (atEventId) {
      queryParams.at = atEventId;
    }
    const queryString = utils.encodeParams(queryParams);
    const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, {
      $roomId: roomId
    });
    return this.http.authedRequest(_httpApi.Method.Get, path);
  }

  /**
   * Upgrades a room to a new protocol version
   * @param newVersion - The target version to upgrade to
   * @returns Promise which resolves: Object with key 'replacement_room'
   * @returns Rejects: with an error response.
   */
  upgradeRoom(roomId, newVersion) {
    // eslint-disable-line camelcase
    const path = utils.encodeUri("/rooms/$roomId/upgrade", {
      $roomId: roomId
    });
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, {
      new_version: newVersion
    });
  }

  /**
   * Retrieve a state event.
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  getStateEvent(roomId, eventType, stateKey) {
    const pathParams = {
      $roomId: roomId,
      $eventType: eventType,
      $stateKey: stateKey
    };
    let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
    if (stateKey !== undefined) {
      path = utils.encodeUri(path + "/$stateKey", pathParams);
    }
    return this.http.authedRequest(_httpApi.Method.Get, path);
  }

  /**
   * @param opts - Options for the request function.
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  sendStateEvent(roomId, eventType, content, stateKey = "", opts = {}) {
    const pathParams = {
      $roomId: roomId,
      $eventType: eventType,
      $stateKey: stateKey
    };
    let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
    if (stateKey !== undefined) {
      path = utils.encodeUri(path + "/$stateKey", pathParams);
    }
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, content, opts);
  }

  /**
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  roomInitialSync(roomId, limit) {
    var _limit$toString;
    const path = utils.encodeUri("/rooms/$roomId/initialSync", {
      $roomId: roomId
    });
    return this.http.authedRequest(_httpApi.Method.Get, path, {
      limit: (_limit$toString = limit === null || limit === void 0 ? void 0 : limit.toString()) !== null && _limit$toString !== void 0 ? _limit$toString : "30"
    });
  }

  /**
   * Set a marker to indicate the point in a room before which the user has read every
   * event. This can be retrieved from room account data (the event type is `m.fully_read`)
   * and displayed as a horizontal line in the timeline that is visually distinct to the
   * position of the user's own read receipt.
   * @param roomId - ID of the room that has been read
   * @param rmEventId - ID of the event that has been read
   * @param rrEventId - ID of the event tracked by the read receipt. This is here
   * for convenience because the RR and the RM are commonly updated at the same time as
   * each other. Optional.
   * @param rpEventId - rpEvent the m.read.private read receipt event for when we
   * don't want other users to see the read receipts. This is experimental. Optional.
   * @returns Promise which resolves: the empty object, `{}`.
   */
  async setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, rpEventId) {
    const path = utils.encodeUri("/rooms/$roomId/read_markers", {
      $roomId: roomId
    });
    const content = {
      [_read_receipts.ReceiptType.FullyRead]: rmEventId,
      [_read_receipts.ReceiptType.Read]: rrEventId
    };
    if ((await this.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) || (await this.isVersionSupported("v1.4"))) {
      content[_read_receipts.ReceiptType.ReadPrivate] = rpEventId;
    }
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, content);
  }

  /**
   * @returns Promise which resolves: A list of the user's current rooms
   * @returns Rejects: with an error response.
   */
  getJoinedRooms() {
    const path = utils.encodeUri("/joined_rooms", {});
    return this.http.authedRequest(_httpApi.Method.Get, path);
  }

  /**
   * Retrieve membership info. for a room.
   * @param roomId - ID of the room to get membership for
   * @returns Promise which resolves: A list of currently joined users
   *                                 and their profile data.
   * @returns Rejects: with an error response.
   */
  getJoinedRoomMembers(roomId) {
    const path = utils.encodeUri("/rooms/$roomId/joined_members", {
      $roomId: roomId
    });
    return this.http.authedRequest(_httpApi.Method.Get, path);
  }

  /**
   * @param options - Options for this request
   * @param server - The remote server to query for the room list.
   *                                Optional. If unspecified, get the local home
   *                                server's public room list.
   * @param limit - Maximum number of entries to return
   * @param since - Token to paginate from
   * @returns Promise which resolves: IPublicRoomsResponse
   * @returns Rejects: with an error response.
   */
  publicRooms(_ref2 = {}) {
    let {
        server,
        limit,
        since
      } = _ref2,
      options = (0, _objectWithoutProperties2.default)(_ref2, _excluded);
    const queryParams = {
      server,
      limit,
      since
    };
    if (Object.keys(options).length === 0) {
      return this.http.authedRequest(_httpApi.Method.Get, "/publicRooms", queryParams);
    } else {
      return this.http.authedRequest(_httpApi.Method.Post, "/publicRooms", queryParams, options);
    }
  }

  /**
   * Create an alias to room ID mapping.
   * @param alias - The room alias to create.
   * @param roomId - The room ID to link the alias to.
   * @returns Promise which resolves: an empty object `{}`
   * @returns Rejects: with an error response.
   */
  createAlias(alias, roomId) {
    const path = utils.encodeUri("/directory/room/$alias", {
      $alias: alias
    });
    const data = {
      room_id: roomId
    };
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, data);
  }

  /**
   * Delete an alias to room ID mapping. This alias must be on your local server,
   * and you must have sufficient access to do this operation.
   * @param alias - The room alias to delete.
   * @returns Promise which resolves: an empty object `{}`.
   * @returns Rejects: with an error response.
   */
  deleteAlias(alias) {
    const path = utils.encodeUri("/directory/room/$alias", {
      $alias: alias
    });
    return this.http.authedRequest(_httpApi.Method.Delete, path);
  }

  /**
   * Gets the local aliases for the room. Note: this includes all local aliases, unlike the
   * curated list from the m.room.canonical_alias state event.
   * @param roomId - The room ID to get local aliases for.
   * @returns Promise which resolves: an object with an `aliases` property, containing an array of local aliases
   * @returns Rejects: with an error response.
   */
  getLocalAliases(roomId) {
    const path = utils.encodeUri("/rooms/$roomId/aliases", {
      $roomId: roomId
    });
    const prefix = _httpApi.ClientPrefix.V3;
    return this.http.authedRequest(_httpApi.Method.Get, path, undefined, undefined, {
      prefix
    });
  }

  /**
   * Get room info for the given alias.
   * @param alias - The room alias to resolve.
   * @returns Promise which resolves: Object with room_id and servers.
   * @returns Rejects: with an error response.
   */
  getRoomIdForAlias(alias) {
    // eslint-disable-line camelcase
    // TODO: deprecate this or resolveRoomAlias
    const path = utils.encodeUri("/directory/room/$alias", {
      $alias: alias
    });
    return this.http.authedRequest(_httpApi.Method.Get, path);
  }

  /**
   * @returns Promise which resolves: Object with room_id and servers.
   * @returns Rejects: with an error response.
   */
  // eslint-disable-next-line camelcase
  resolveRoomAlias(roomAlias) {
    // TODO: deprecate this or getRoomIdForAlias
    const path = utils.encodeUri("/directory/room/$alias", {
      $alias: roomAlias
    });
    return this.http.request(_httpApi.Method.Get, path);
  }

  /**
   * Get the visibility of a room in the current HS's room directory
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  getRoomDirectoryVisibility(roomId) {
    const path = utils.encodeUri("/directory/list/room/$roomId", {
      $roomId: roomId
    });
    return this.http.authedRequest(_httpApi.Method.Get, path);
  }

  /**
   * Set the visbility of a room in the current HS's room directory
   * @param visibility - "public" to make the room visible
   *                 in the public directory, or "private" to make
   *                 it invisible.
   * @returns Promise which resolves: to an empty object `{}`
   * @returns Rejects: with an error response.
   */
  setRoomDirectoryVisibility(roomId, visibility) {
    const path = utils.encodeUri("/directory/list/room/$roomId", {
      $roomId: roomId
    });
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, {
      visibility
    });
  }

  /**
   * Set the visbility of a room bridged to a 3rd party network in
   * the current HS's room directory.
   * @param networkId - the network ID of the 3rd party
   *                 instance under which this room is published under.
   * @param visibility - "public" to make the room visible
   *                 in the public directory, or "private" to make
   *                 it invisible.
   * @returns Promise which resolves: result object
   * @returns Rejects: with an error response.
   */
  setRoomDirectoryVisibilityAppService(networkId, roomId, visibility) {
    // TODO: Types
    const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", {
      $networkId: networkId,
      $roomId: roomId
    });
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, {
      visibility: visibility
    });
  }

  /**
   * Query the user directory with a term matching user IDs, display names and domains.
   * @param term - the term with which to search.
   * @param limit - the maximum number of results to return. The server will
   *                 apply a limit if unspecified.
   * @returns Promise which resolves: an array of results.
   */
  searchUserDirectory({
    term,
    limit
  }) {
    const body = {
      search_term: term
    };
    if (limit !== undefined) {
      body.limit = limit;
    }
    return this.http.authedRequest(_httpApi.Method.Post, "/user_directory/search", undefined, body);
  }

  /**
   * Upload a file to the media repository on the homeserver.
   *
   * @param file - The object to upload. On a browser, something that
   *   can be sent to XMLHttpRequest.send (typically a File).  Under node.js,
   *   a a Buffer, String or ReadStream.
   *
   * @param opts -  options object
   *
   * @returns Promise which resolves to response object, as
   *    determined by this.opts.onlyData, opts.rawResponse, and
   *    opts.onlyContentUri.  Rejects with an error (usually a MatrixError).
   */
  uploadContent(file, opts) {
    return this.http.uploadContent(file, opts);
  }

  /**
   * Cancel a file upload in progress
   * @param upload - The object returned from uploadContent
   * @returns true if canceled, otherwise false
   */
  cancelUpload(upload) {
    return this.http.cancelUpload(upload);
  }

  /**
   * Get a list of all file uploads in progress
   * @returns Array of objects representing current uploads.
   * Currently in progress is element 0. Keys:
   *  - promise: The promise associated with the upload
   *  - loaded: Number of bytes uploaded
   *  - total: Total number of bytes to upload
   */
  getCurrentUploads() {
    return this.http.getCurrentUploads();
  }

  /**
   * @param info - The kind of info to retrieve (e.g. 'displayname',
   * 'avatar_url').
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   */
  getProfileInfo(userId, info
  // eslint-disable-next-line camelcase
  ) {
    const path = info ? utils.encodeUri("/profile/$userId/$info", {
      $userId: userId,
      $info: info
    }) : utils.encodeUri("/profile/$userId", {
      $userId: userId
    });
    return this.http.authedRequest(_httpApi.Method.Get, path);
  }

  /**
   * @returns Promise which resolves to a list of the user's threepids.
   * @returns Rejects: with an error response.
   */
  getThreePids() {
    return this.http.authedRequest(_httpApi.Method.Get, "/account/3pid");
  }

  /**
   * Add a 3PID to your homeserver account and optionally bind it to an identity
   * server as well. An identity server is required as part of the `creds` object.
   *
   * This API is deprecated, and you should instead use `addThreePidOnly`
   * for homeservers that support it.
   *
   * @returns Promise which resolves: on success
   * @returns Rejects: with an error response.
   */
  addThreePid(creds, bind) {
    // TODO: Types
    const path = "/account/3pid";
    const data = {
      threePidCreds: creds,
      bind: bind
    };
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data);
  }

  /**
   * Add a 3PID to your homeserver account. This API does not use an identity
   * server, as the homeserver is expected to handle 3PID ownership validation.
   *
   * You can check whether a homeserver supports this API via
   * `doesServerSupportSeparateAddAndBind`.
   *
   * @param data - A object with 3PID validation data from having called
   * `account/3pid/<medium>/requestToken` on the homeserver.
   * @returns Promise which resolves: to an empty object `{}`
   * @returns Rejects: with an error response.
   */
  async addThreePidOnly(data) {
    const path = "/account/3pid/add";
    const prefix = (await this.isVersionSupported("r0.6.0")) ? _httpApi.ClientPrefix.R0 : _httpApi.ClientPrefix.Unstable;
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data, {
      prefix
    });
  }

  /**
   * Bind a 3PID for discovery onto an identity server via the homeserver. The
   * identity server handles 3PID ownership validation and the homeserver records
   * the new binding to track where all 3PIDs for the account are bound.
   *
   * You can check whether a homeserver supports this API via
   * `doesServerSupportSeparateAddAndBind`.
   *
   * @param data - A object with 3PID validation data from having called
   * `validate/<medium>/requestToken` on the identity server. It should also
   * contain `id_server` and `id_access_token` fields as well.
   * @returns Promise which resolves: to an empty object `{}`
   * @returns Rejects: with an error response.
   */
  async bindThreePid(data) {
    const path = "/account/3pid/bind";
    const prefix = (await this.isVersionSupported("r0.6.0")) ? _httpApi.ClientPrefix.R0 : _httpApi.ClientPrefix.Unstable;
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data, {
      prefix
    });
  }

  /**
   * Unbind a 3PID for discovery on an identity server via the homeserver. The
   * homeserver removes its record of the binding to keep an updated record of
   * where all 3PIDs for the account are bound.
   *
   * @param medium - The threepid medium (eg. 'email')
   * @param address - The threepid address (eg. 'bob\@example.com')
   *        this must be as returned by getThreePids.
   * @returns Promise which resolves: on success
   * @returns Rejects: with an error response.
   */
  async unbindThreePid(medium, address
  // eslint-disable-next-line camelcase
  ) {
    const path = "/account/3pid/unbind";
    const data = {
      medium,
      address,
      id_server: this.getIdentityServerUrl(true)
    };
    const prefix = (await this.isVersionSupported("r0.6.0")) ? _httpApi.ClientPrefix.R0 : _httpApi.ClientPrefix.Unstable;
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data, {
      prefix
    });
  }

  /**
   * @param medium - The threepid medium (eg. 'email')
   * @param address - The threepid address (eg. 'bob\@example.com')
   *        this must be as returned by getThreePids.
   * @returns Promise which resolves: The server response on success
   *     (generally the empty JSON object)
   * @returns Rejects: with an error response.
   */
  deleteThreePid(medium, address
  // eslint-disable-next-line camelcase
  ) {
    const path = "/account/3pid/delete";
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, {
      medium,
      address
    });
  }

  /**
   * Make a request to change your password.
   * @param newPassword - The new desired password.
   * @param logoutDevices - Should all sessions be logged out after the password change. Defaults to true.
   * @returns Promise which resolves: to an empty object `{}`
   * @returns Rejects: with an error response.
   */
  setPassword(authDict, newPassword, logoutDevices) {
    const path = "/account/password";
    const data = {
      auth: authDict,
      new_password: newPassword,
      logout_devices: logoutDevices
    };
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data);
  }

  /**
   * Gets all devices recorded for the logged-in user
   * @returns Promise which resolves: result object
   * @returns Rejects: with an error response.
   */
  getDevices() {
    return this.http.authedRequest(_httpApi.Method.Get, "/devices");
  }

  /**
   * Gets specific device details for the logged-in user
   * @param deviceId -  device to query
   * @returns Promise which resolves: result object
   * @returns Rejects: with an error response.
   */
  getDevice(deviceId) {
    const path = utils.encodeUri("/devices/$device_id", {
      $device_id: deviceId
    });
    return this.http.authedRequest(_httpApi.Method.Get, path);
  }

  /**
   * Update the given device
   *
   * @param deviceId -  device to update
   * @param body -       body of request
   * @returns Promise which resolves: to an empty object `{}`
   * @returns Rejects: with an error response.
   */
  // eslint-disable-next-line camelcase
  setDeviceDetails(deviceId, body) {
    const path = utils.encodeUri("/devices/$device_id", {
      $device_id: deviceId
    });
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, body);
  }

  /**
   * Delete the given device
   *
   * @param deviceId -  device to delete
   * @param auth - Optional. Auth data to supply for User-Interactive auth.
   * @returns Promise which resolves: result object
   * @returns Rejects: with an error response.
   */
  deleteDevice(deviceId, auth) {
    const path = utils.encodeUri("/devices/$device_id", {
      $device_id: deviceId
    });
    const body = {};
    if (auth) {
      body.auth = auth;
    }
    return this.http.authedRequest(_httpApi.Method.Delete, path, undefined, body);
  }

  /**
   * Delete multiple device
   *
   * @param devices - IDs of the devices to delete
   * @param auth - Optional. Auth data to supply for User-Interactive auth.
   * @returns Promise which resolves: result object
   * @returns Rejects: with an error response.
   */
  deleteMultipleDevices(devices, auth) {
    const body = {
      devices
    };
    if (auth) {
      body.auth = auth;
    }
    const path = "/delete_devices";
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, body);
  }

  /**
   * Gets all pushers registered for the logged-in user
   *
   * @returns Promise which resolves: Array of objects representing pushers
   * @returns Rejects: with an error response.
   */
  async getPushers() {
    const response = await this.http.authedRequest(_httpApi.Method.Get, "/pushers");

    // Migration path for clients that connect to a homeserver that does not support
    // MSC3881 yet, see https://github.com/matrix-org/matrix-spec-proposals/blob/kerry/remote-push-toggle/proposals/3881-remote-push-notification-toggling.md#migration
    if (!(await this.doesServerSupportUnstableFeature("org.matrix.msc3881"))) {
      response.pushers = response.pushers.map(pusher => {
        if (!pusher.hasOwnProperty(_event2.PUSHER_ENABLED.name)) {
          pusher[_event2.PUSHER_ENABLED.name] = true;
        }
        return pusher;
      });
    }
    return response;
  }

  /**
   * Adds a new pusher or updates an existing pusher
   *
   * @param pusher - Object representing a pusher
   * @returns Promise which resolves: Empty json object on success
   * @returns Rejects: with an error response.
   */
  setPusher(pusher) {
    const path = "/pushers/set";
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, pusher);
  }

  /**
   * Persists local notification settings
   * @returns Promise which resolves: an empty object
   * @returns Rejects: with an error response.
   */
  setLocalNotificationSettings(deviceId, notificationSettings) {
    const key = `${_event2.LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
    return this.setAccountData(key, notificationSettings);
  }

  /**
   * Get the push rules for the account from the server.
   * @returns Promise which resolves to the push rules.
   * @returns Rejects: with an error response.
   */
  getPushRules() {
    return this.http.authedRequest(_httpApi.Method.Get, "/pushrules/").then(rules => {
      this.setPushRules(rules);
      return this.pushRules;
    });
  }

  /**
   * Update the push rules for the account. This should be called whenever
   * updated push rules are available.
   */
  setPushRules(rules) {
    // Fix-up defaults, if applicable.
    this.pushRules = _pushprocessor.PushProcessor.rewriteDefaultRules(rules, this.getUserId());
    // Pre-calculate any necessary caches.
    this.pushProcessor.updateCachedPushRuleKeys(this.pushRules);
  }

  /**
   * @returns Promise which resolves: an empty object `{}`
   * @returns Rejects: with an error response.
   */
  addPushRule(scope, kind, ruleId, body) {
    // NB. Scope not uri encoded because devices need the '/'
    const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
      $kind: kind,
      $ruleId: ruleId
    });
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, body);
  }

  /**
   * @returns Promise which resolves: an empty object `{}`
   * @returns Rejects: with an error response.
   */
  deletePushRule(scope, kind, ruleId) {
    // NB. Scope not uri encoded because devices need the '/'
    const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
      $kind: kind,
      $ruleId: ruleId
    });
    return this.http.authedRequest(_httpApi.Method.Delete, path);
  }

  /**
   * Enable or disable a push notification rule.
   * @returns Promise which resolves: to an empty object `{}`
   * @returns Rejects: with an error response.
   */
  setPushRuleEnabled(scope, kind, ruleId, enabled) {
    const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", {
      $kind: kind,
      $ruleId: ruleId
    });
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, {
      enabled: enabled
    });
  }

  /**
   * Set the actions for a push notification rule.
   * @returns Promise which resolves: to an empty object `{}`
   * @returns Rejects: with an error response.
   */
  setPushRuleActions(scope, kind, ruleId, actions) {
    const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", {
      $kind: kind,
      $ruleId: ruleId
    });
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, {
      actions: actions
    });
  }

  /**
   * Perform a server-side search.
   * @param next_batch - the batch token to pass in the query string
   * @param body - the JSON object to pass to the request body.
   * @param abortSignal - optional signal used to cancel the http request.
   * @returns Promise which resolves to the search response object.
   * @returns Rejects: with an error response.
   */
  search({
    body,
    next_batch: nextBatch
  }, abortSignal) {
    const queryParams = {};
    if (nextBatch) {
      queryParams.next_batch = nextBatch;
    }
    return this.http.authedRequest(_httpApi.Method.Post, "/search", queryParams, body, {
      abortSignal
    });
  }

  /**
   * Upload keys
   *
   * @param content -  body of upload request
   *
   * @param opts - this method no longer takes any opts,
   *  used to take opts.device_id but this was not removed from the spec as a redundant parameter
   *
   * @returns Promise which resolves: result object. Rejects: with
   *     an error response ({@link MatrixError}).
   */
  uploadKeysRequest(content, opts) {
    return this.http.authedRequest(_httpApi.Method.Post, "/keys/upload", undefined, content);
  }
  uploadKeySignatures(content) {
    return this.http.authedRequest(_httpApi.Method.Post, "/keys/signatures/upload", undefined, content, {
      prefix: _httpApi.ClientPrefix.V3
    });
  }

  /**
   * Download device keys
   *
   * @param userIds -  list of users to get keys for
   *
   * @param token - sync token to pass in the query request, to help
   *   the HS give the most recent results
   *
   * @returns Promise which resolves: result object. Rejects: with
   *     an error response ({@link MatrixError}).
   */
  downloadKeysForUsers(userIds, {
    token
  } = {}) {
    const content = {
      device_keys: {}
    };
    if (token !== undefined) {
      content.token = token;
    }
    userIds.forEach(u => {
      content.device_keys[u] = [];
    });
    return this.http.authedRequest(_httpApi.Method.Post, "/keys/query", undefined, content);
  }

  /**
   * Claim one-time keys
   *
   * @param devices -  a list of [userId, deviceId] pairs
   *
   * @param keyAlgorithm -  desired key type
   *
   * @param timeout - the time (in milliseconds) to wait for keys from remote
   *     servers
   *
   * @returns Promise which resolves: result object. Rejects: with
   *     an error response ({@link MatrixError}).
   */
  claimOneTimeKeys(devices, keyAlgorithm = "signed_curve25519", timeout) {
    const queries = {};
    if (keyAlgorithm === undefined) {
      keyAlgorithm = "signed_curve25519";
    }
    for (const [userId, deviceId] of devices) {
      const query = queries[userId] || {};
      queries[userId] = query;
      query[deviceId] = keyAlgorithm;
    }
    const content = {
      one_time_keys: queries
    };
    if (timeout) {
      content.timeout = timeout;
    }
    const path = "/keys/claim";
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, content);
  }

  /**
   * Ask the server for a list of users who have changed their device lists
   * between a pair of sync tokens
   *
   *
   * @returns Promise which resolves: result object. Rejects: with
   *     an error response ({@link MatrixError}).
   */
  getKeyChanges(oldToken, newToken) {
    const qps = {
      from: oldToken,
      to: newToken
    };
    return this.http.authedRequest(_httpApi.Method.Get, "/keys/changes", qps);
  }
  uploadDeviceSigningKeys(auth, keys) {
    // API returns empty object
    const data = Object.assign({}, keys);
    if (auth) Object.assign(data, {
      auth
    });
    return this.http.authedRequest(_httpApi.Method.Post, "/keys/device_signing/upload", undefined, data, {
      prefix: _httpApi.ClientPrefix.Unstable
    });
  }

  /**
   * Register with an identity server using the OpenID token from the user's
   * Homeserver, which can be retrieved via
   * {@link MatrixClient#getOpenIdToken}.
   *
   * Note that the `/account/register` endpoint (as well as IS authentication in
   * general) was added as part of the v2 API version.
   *
   * @returns Promise which resolves: with object containing an Identity
   * Server access token.
   * @returns Rejects: with an error response.
   */
  registerWithIdentityServer(hsOpenIdToken) {
    if (!this.idBaseUrl) {
      throw new Error("No identity server base URL set");
    }
    const uri = this.http.getUrl("/account/register", undefined, _httpApi.IdentityPrefix.V2, this.idBaseUrl);
    return this.http.requestOtherUrl(_httpApi.Method.Post, uri, hsOpenIdToken);
  }

  /**
   * Requests an email verification token directly from an identity server.
   *
   * This API is used as part of binding an email for discovery on an identity
   * server. The validation data that results should be passed to the
   * `bindThreePid` method to complete the binding process.
   *
   * @param email - The email address to request a token for
   * @param clientSecret - A secret binary string generated by the client.
   *                 It is recommended this be around 16 ASCII characters.
   * @param sendAttempt - If an identity server sees a duplicate request
   *                 with the same sendAttempt, it will not send another email.
   *                 To request another email to be sent, use a larger value for
   *                 the sendAttempt param as was used in the previous request.
   * @param nextLink - Optional If specified, the client will be redirected
   *                 to this link after validation.
   * @param identityAccessToken - The `access_token` field of the identity
   * server `/account/register` response (see {@link registerWithIdentityServer}).
   *
   * @returns Promise which resolves: TODO
   * @returns Rejects: with an error response.
   * @throws Error if no identity server is set
   */
  requestEmailToken(email, clientSecret, sendAttempt, nextLink, identityAccessToken) {
    const params = {
      client_secret: clientSecret,
      email: email,
      send_attempt: sendAttempt === null || sendAttempt === void 0 ? void 0 : sendAttempt.toString()
    };
    if (nextLink) {
      params.next_link = nextLink;
    }
    return this.http.idServerRequest(_httpApi.Method.Post, "/validate/email/requestToken", params, _httpApi.IdentityPrefix.V2, identityAccessToken);
  }

  /**
   * Requests a MSISDN verification token directly from an identity server.
   *
   * This API is used as part of binding a MSISDN for discovery on an identity
   * server. The validation data that results should be passed to the
   * `bindThreePid` method to complete the binding process.
   *
   * @param phoneCountry - The ISO 3166-1 alpha-2 code for the country in
   *                 which phoneNumber should be parsed relative to.
   * @param phoneNumber - The phone number, in national or international
   *                 format
   * @param clientSecret - A secret binary string generated by the client.
   *                 It is recommended this be around 16 ASCII characters.
   * @param sendAttempt - If an identity server sees a duplicate request
   *                 with the same sendAttempt, it will not send another SMS.
   *                 To request another SMS to be sent, use a larger value for
   *                 the sendAttempt param as was used in the previous request.
   * @param nextLink - Optional If specified, the client will be redirected
   *                 to this link after validation.
   * @param identityAccessToken - The `access_token` field of the Identity
   * Server `/account/register` response (see {@link registerWithIdentityServer}).
   *
   * @returns Promise which resolves to an object with a sid string
   * @returns Rejects: with an error response.
   * @throws Error if no identity server is set
   */
  requestMsisdnToken(phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink, identityAccessToken) {
    const params = {
      client_secret: clientSecret,
      country: phoneCountry,
      phone_number: phoneNumber,
      send_attempt: sendAttempt === null || sendAttempt === void 0 ? void 0 : sendAttempt.toString()
    };
    if (nextLink) {
      params.next_link = nextLink;
    }
    return this.http.idServerRequest(_httpApi.Method.Post, "/validate/msisdn/requestToken", params, _httpApi.IdentityPrefix.V2, identityAccessToken);
  }

  /**
   * Submits a MSISDN token to the identity server
   *
   * This is used when submitting the code sent by SMS to a phone number.
   * The identity server has an equivalent API for email but the js-sdk does
   * not expose this, since email is normally validated by the user clicking
   * a link rather than entering a code.
   *
   * @param sid - The sid given in the response to requestToken
   * @param clientSecret - A secret binary string generated by the client.
   *                 This must be the same value submitted in the requestToken call.
   * @param msisdnToken - The MSISDN token, as enetered by the user.
   * @param identityAccessToken - The `access_token` field of the Identity
   * Server `/account/register` response (see {@link registerWithIdentityServer}).
   *
   * @returns Promise which resolves: Object, currently with no parameters.
   * @returns Rejects: with an error response.
   * @throws Error if No identity server is set
   */
  submitMsisdnToken(sid, clientSecret, msisdnToken, identityAccessToken) {
    // TODO: Types
    const params = {
      sid: sid,
      client_secret: clientSecret,
      token: msisdnToken
    };
    return this.http.idServerRequest(_httpApi.Method.Post, "/validate/msisdn/submitToken", params, _httpApi.IdentityPrefix.V2, identityAccessToken);
  }

  /**
   * Submits a MSISDN token to an arbitrary URL.
   *
   * This is used when submitting the code sent by SMS to a phone number in the
   * newer 3PID flow where the homeserver validates 3PID ownership (as part of
   * `requestAdd3pidMsisdnToken`). The homeserver response may include a
   * `submit_url` to specify where the token should be sent, and this helper can
   * be used to pass the token to this URL.
   *
   * @param url - The URL to submit the token to
   * @param sid - The sid given in the response to requestToken
   * @param clientSecret - A secret binary string generated by the client.
   *                 This must be the same value submitted in the requestToken call.
   * @param msisdnToken - The MSISDN token, as enetered by the user.
   *
   * @returns Promise which resolves: Object, currently with no parameters.
   * @returns Rejects: with an error response.
   */
  submitMsisdnTokenOtherUrl(url, sid, clientSecret, msisdnToken) {
    // TODO: Types
    const params = {
      sid: sid,
      client_secret: clientSecret,
      token: msisdnToken
    };
    return this.http.requestOtherUrl(_httpApi.Method.Post, url, params);
  }

  /**
   * Gets the V2 hashing information from the identity server. Primarily useful for
   * lookups.
   * @param identityAccessToken - The access token for the identity server.
   * @returns The hashing information for the identity server.
   */
  getIdentityHashDetails(identityAccessToken) {
    // TODO: Types
    return this.http.idServerRequest(_httpApi.Method.Get, "/hash_details", undefined, _httpApi.IdentityPrefix.V2, identityAccessToken);
  }

  /**
   * Performs a hashed lookup of addresses against the identity server. This is
   * only supported on identity servers which have at least the version 2 API.
   * @param addressPairs - An array of 2 element arrays.
   * The first element of each pair is the address, the second is the 3PID medium.
   * Eg: `["email@example.org", "email"]`
   * @param identityAccessToken - The access token for the identity server.
   * @returns A collection of address mappings to
   * found MXIDs. Results where no user could be found will not be listed.
   */
  async identityHashedLookup(addressPairs, identityAccessToken) {
    const params = {
      // addresses: ["email@example.org", "10005550000"],
      // algorithm: "sha256",
      // pepper: "abc123"
    };

    // Get hash information first before trying to do a lookup
    const hashes = await this.getIdentityHashDetails(identityAccessToken);
    if (!hashes || !hashes["lookup_pepper"] || !hashes["algorithms"]) {
      throw new Error("Unsupported identity server: bad response");
    }
    params["pepper"] = hashes["lookup_pepper"];
    const localMapping = {
      // hashed identifier => plain text address
      // For use in this function's return format
    };

    // When picking an algorithm, we pick the hashed over no hashes
    if (hashes["algorithms"].includes("sha256")) {
      // Abuse the olm hashing
      const olmutil = new global.Olm.Utility();
      params["addresses"] = addressPairs.map(p => {
        const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
        const med = p[1].toLowerCase();
        const hashed = olmutil.sha256(`${addr} ${med} ${params["pepper"]}`).replace(/\+/g, "-").replace(/\//g, "_"); // URL-safe base64
        // Map the hash to a known (case-sensitive) address. We use the case
        // sensitive version because the caller might be expecting that.
        localMapping[hashed] = p[0];
        return hashed;
      });
      params["algorithm"] = "sha256";
    } else if (hashes["algorithms"].includes("none")) {
      params["addresses"] = addressPairs.map(p => {
        const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
        const med = p[1].toLowerCase();
        const unhashed = `${addr} ${med}`;
        // Map the unhashed values to a known (case-sensitive) address. We use
        // the case-sensitive version because the caller might be expecting that.
        localMapping[unhashed] = p[0];
        return unhashed;
      });
      params["algorithm"] = "none";
    } else {
      throw new Error("Unsupported identity server: unknown hash algorithm");
    }
    const response = await this.http.idServerRequest(_httpApi.Method.Post, "/lookup", params, _httpApi.IdentityPrefix.V2, identityAccessToken);
    if (!(response !== null && response !== void 0 && response["mappings"])) return []; // no results

    const foundAddresses = [];
    for (const hashed of Object.keys(response["mappings"])) {
      const mxid = response["mappings"][hashed];
      const plainAddress = localMapping[hashed];
      if (!plainAddress) {
        throw new Error("Identity server returned more results than expected");
      }
      foundAddresses.push({
        address: plainAddress,
        mxid
      });
    }
    return foundAddresses;
  }

  /**
   * Looks up the public Matrix ID mapping for a given 3rd party
   * identifier from the identity server
   *
   * @param medium - The medium of the threepid, eg. 'email'
   * @param address - The textual address of the threepid
   * @param identityAccessToken - The `access_token` field of the Identity
   * Server `/account/register` response (see {@link registerWithIdentityServer}).
   *
   * @returns Promise which resolves: A threepid mapping
   *                                 object or the empty object if no mapping
   *                                 exists
   * @returns Rejects: with an error response.
   */
  async lookupThreePid(medium, address, identityAccessToken) {
    // TODO: Types
    // Note: we're using the V2 API by calling this function, but our
    // function contract requires a V1 response. We therefore have to
    // convert it manually.
    const response = await this.identityHashedLookup([[address, medium]], identityAccessToken);
    const result = response.find(p => p.address === address);
    if (!result) {
      return {};
    }
    const mapping = {
      address,
      medium,
      mxid: result.mxid

      // We can't reasonably fill these parameters:
      // not_before
      // not_after
      // ts
      // signatures
    };

    return mapping;
  }

  /**
   * Looks up the public Matrix ID mappings for multiple 3PIDs.
   *
   * @param query - Array of arrays containing
   * [medium, address]
   * @param identityAccessToken - The `access_token` field of the Identity
   * Server `/account/register` response (see {@link registerWithIdentityServer}).
   *
   * @returns Promise which resolves: Lookup results from IS.
   * @returns Rejects: with an error response.
   */
  async bulkLookupThreePids(query, identityAccessToken) {
    // TODO: Types
    // Note: we're using the V2 API by calling this function, but our
    // function contract requires a V1 response. We therefore have to
    // convert it manually.
    const response = await this.identityHashedLookup(
    // We have to reverse the query order to get [address, medium] pairs
    query.map(p => [p[1], p[0]]), identityAccessToken);
    const v1results = [];
    for (const mapping of response) {
      const originalQuery = query.find(p => p[1] === mapping.address);
      if (!originalQuery) {
        throw new Error("Identity sever returned unexpected results");
      }
      v1results.push([originalQuery[0],
      // medium
      mapping.address, mapping.mxid]);
    }
    return {
      threepids: v1results
    };
  }

  /**
   * Get account info from the identity server. This is useful as a neutral check
   * to verify that other APIs are likely to approve access by testing that the
   * token is valid, terms have been agreed, etc.
   *
   * @param identityAccessToken - The `access_token` field of the Identity
   * Server `/account/register` response (see {@link registerWithIdentityServer}).
   *
   * @returns Promise which resolves: an object with account info.
   * @returns Rejects: with an error response.
   */
  getIdentityAccount(identityAccessToken) {
    // TODO: Types
    return this.http.idServerRequest(_httpApi.Method.Get, "/account", undefined, _httpApi.IdentityPrefix.V2, identityAccessToken);
  }

  /**
   * Send an event to a specific list of devices.
   * This is a low-level API that simply wraps the HTTP API
   * call to send to-device messages. We recommend using
   * queueToDevice() which is a higher level API.
   *
   * @param eventType -  type of event to send
   *    content to send. Map from user_id to device_id to content object.
   * @param txnId -     transaction id. One will be made up if not
   *    supplied.
   * @returns Promise which resolves: to an empty object `{}`
   */
  sendToDevice(eventType, contentMap, txnId) {
    const path = utils.encodeUri("/sendToDevice/$eventType/$txnId", {
      $eventType: eventType,
      $txnId: txnId ? txnId : this.makeTxnId()
    });
    const body = {
      messages: utils.recursiveMapToObject(contentMap)
    };
    const targets = new Map();
    for (const [userId, deviceMessages] of contentMap) {
      targets.set(userId, Array.from(deviceMessages.keys()));
    }
    _logger.logger.log(`PUT ${path}`, targets);
    return this.http.authedRequest(_httpApi.Method.Put, path, undefined, body);
  }

  /**
   * Sends events directly to specific devices using Matrix's to-device
   * messaging system. The batch will be split up into appropriately sized
   * batches for sending and stored in the store so they can be retried
   * later if they fail to send. Retries will happen automatically.
   * @param batch - The to-device messages to send
   */
  queueToDevice(batch) {
    return this.toDeviceMessageQueue.queueBatch(batch);
  }

  /**
   * Get the third party protocols that can be reached using
   * this HS
   * @returns Promise which resolves to the result object
   */
  getThirdpartyProtocols() {
    return this.http.authedRequest(_httpApi.Method.Get, "/thirdparty/protocols").then(response => {
      // sanity check
      if (!response || typeof response !== "object") {
        throw new Error(`/thirdparty/protocols did not return an object: ${response}`);
      }
      return response;
    });
  }

  /**
   * Get information on how a specific place on a third party protocol
   * may be reached.
   * @param protocol - The protocol given in getThirdpartyProtocols()
   * @param params - Protocol-specific parameters, as given in the
   *                        response to getThirdpartyProtocols()
   * @returns Promise which resolves to the result object
   */
  getThirdpartyLocation(protocol, params) {
    const path = utils.encodeUri("/thirdparty/location/$protocol", {
      $protocol: protocol
    });
    return this.http.authedRequest(_httpApi.Method.Get, path, params);
  }

  /**
   * Get information on how a specific user on a third party protocol
   * may be reached.
   * @param protocol - The protocol given in getThirdpartyProtocols()
   * @param params - Protocol-specific parameters, as given in the
   *                        response to getThirdpartyProtocols()
   * @returns Promise which resolves to the result object
   */
  getThirdpartyUser(protocol, params) {
    // TODO: Types
    const path = utils.encodeUri("/thirdparty/user/$protocol", {
      $protocol: protocol
    });
    return this.http.authedRequest(_httpApi.Method.Get, path, params);
  }
  getTerms(serviceType, baseUrl) {
    // TODO: Types
    const url = this.termsUrlForService(serviceType, baseUrl);
    return this.http.requestOtherUrl(_httpApi.Method.Get, url);
  }
  agreeToTerms(serviceType, baseUrl, accessToken, termsUrls) {
    const url = this.termsUrlForService(serviceType, baseUrl);
    const headers = {
      Authorization: "Bearer " + accessToken
    };
    return this.http.requestOtherUrl(_httpApi.Method.Post, url, {
      user_accepts: termsUrls
    }, {
      headers
    });
  }

  /**
   * Reports an event as inappropriate to the server, which may then notify the appropriate people.
   * @param roomId - The room in which the event being reported is located.
   * @param eventId - The event to report.
   * @param score - The score to rate this content as where -100 is most offensive and 0 is inoffensive.
   * @param reason - The reason the content is being reported. May be blank.
   * @returns Promise which resolves to an empty object if successful
   */
  reportEvent(roomId, eventId, score, reason) {
    const path = utils.encodeUri("/rooms/$roomId/report/$eventId", {
      $roomId: roomId,
      $eventId: eventId
    });
    return this.http.authedRequest(_httpApi.Method.Post, path, undefined, {
      score,
      reason
    });
  }

  /**
   * Fetches or paginates a room hierarchy as defined by MSC2946.
   * Falls back gracefully to sourcing its data from `getSpaceSummary` if this API is not yet supported by the server.
   * @param roomId - The ID of the space-room to use as the root of the summary.
   * @param limit - The maximum number of rooms to return per page.
   * @param maxDepth - The maximum depth in the tree from the root room to return.
   * @param suggestedOnly - Whether to only return rooms with suggested=true.
   * @param fromToken - The opaque token to paginate a previous request.
   * @returns the response, with next_batch & rooms fields.
   */
  getRoomHierarchy(roomId, limit, maxDepth, suggestedOnly = false, fromToken) {
    const path = utils.encodeUri("/rooms/$roomId/hierarchy", {
      $roomId: roomId
    });
    const queryParams = {
      suggested_only: String(suggestedOnly),
      max_depth: maxDepth === null || maxDepth === void 0 ? void 0 : maxDepth.toString(),
      from: fromToken,
      limit: limit === null || limit === void 0 ? void 0 : limit.toString()
    };
    return this.http.authedRequest(_httpApi.Method.Get, path, queryParams, undefined, {
      prefix: _httpApi.ClientPrefix.V1
    }).catch(e => {
      if (e.errcode === "M_UNRECOGNIZED") {
        // fall back to the prefixed hierarchy API.
        return this.http.authedRequest(_httpApi.Method.Get, path, queryParams, undefined, {
          prefix: "/_matrix/client/unstable/org.matrix.msc2946"
        });
      }
      throw e;
    });
  }

  /**
   * Creates a new file tree space with the given name. The client will pick
   * defaults for how it expects to be able to support the remaining API offered
   * by the returned class.
   *
   * Note that this is UNSTABLE and may have breaking changes without notice.
   * @param name - The name of the tree space.
   * @returns Promise which resolves to the created space.
   */
  async unstableCreateFileTree(name) {
    const {
      room_id: roomId
    } = await this.createRoom({
      name: name,
      preset: _partials.Preset.PrivateChat,
      power_level_content_override: _objectSpread(_objectSpread({}, _MSC3089TreeSpace.DEFAULT_TREE_POWER_LEVELS_TEMPLATE), {}, {
        users: {
          [this.getUserId()]: 100
        }
      }),
      creation_content: {
        [_event2.RoomCreateTypeField]: _event2.RoomType.Space
      },
      initial_state: [{
        type: _event2.UNSTABLE_MSC3088_PURPOSE.name,
        state_key: _event2.UNSTABLE_MSC3089_TREE_SUBTYPE.name,
        content: {
          [_event2.UNSTABLE_MSC3088_ENABLED.name]: true
        }
      }, {
        type: _event2.EventType.RoomEncryption,
        state_key: "",
        content: {
          algorithm: olmlib.MEGOLM_ALGORITHM
        }
      }]
    });
    return new _MSC3089TreeSpace.MSC3089TreeSpace(this, roomId);
  }

  /**
   * Gets a reference to a tree space, if the room ID given is a tree space. If the room
   * does not appear to be a tree space then null is returned.
   *
   * Note that this is UNSTABLE and may have breaking changes without notice.
   * @param roomId - The room ID to get a tree space reference for.
   * @returns The tree space, or null if not a tree space.
   */
  unstableGetFileTreeSpace(roomId) {
    var _purposeEvent$getCont, _createEvent$getConte;
    const room = this.getRoom(roomId);
    if ((room === null || room === void 0 ? void 0 : room.getMyMembership()) !== "join") return null;
    const createEvent = room.currentState.getStateEvents(_event2.EventType.RoomCreate, "");
    const purposeEvent = room.currentState.getStateEvents(_event2.UNSTABLE_MSC3088_PURPOSE.name, _event2.UNSTABLE_MSC3089_TREE_SUBTYPE.name);
    if (!createEvent) throw new Error("Expected single room create event");
    if (!(purposeEvent !== null && purposeEvent !== void 0 && (_purposeEvent$getCont = purposeEvent.getContent()) !== null && _purposeEvent$getCont !== void 0 && _purposeEvent$getCont[_event2.UNSTABLE_MSC3088_ENABLED.name])) return null;
    if (((_createEvent$getConte = createEvent.getContent()) === null || _createEvent$getConte === void 0 ? void 0 : _createEvent$getConte[_event2.RoomCreateTypeField]) !== _event2.RoomType.Space) return null;
    return new _MSC3089TreeSpace.MSC3089TreeSpace(this, roomId);
  }

  /**
   * Perform a single MSC3575 sliding sync request.
   * @param req - The request to make.
   * @param proxyBaseUrl - The base URL for the sliding sync proxy.
   * @param abortSignal - Optional signal to abort request mid-flight.
   * @returns The sliding sync response, or a standard error.
   * @throws on non 2xx status codes with an object with a field "httpStatus":number.
   */
  slidingSync(req, proxyBaseUrl, abortSignal) {
    const qps = {};
    if (req.pos) {
      qps.pos = req.pos;
      delete req.pos;
    }
    if (req.timeout) {
      qps.timeout = req.timeout;
      delete req.timeout;
    }
    const clientTimeout = req.clientTimeout;
    delete req.clientTimeout;
    return this.http.authedRequest(_httpApi.Method.Post, "/sync", qps, req, {
      prefix: "/_matrix/client/unstable/org.matrix.msc3575",
      baseUrl: proxyBaseUrl,
      localTimeoutMs: clientTimeout,
      abortSignal
    });
  }

  /**
   * @deprecated use supportsThreads() instead
   */
  supportsExperimentalThreads() {
    var _this$clientOpts7;
    _logger.logger.warn(`supportsExperimentalThreads() is deprecated, use supportThreads() instead`);
    return ((_this$clientOpts7 = this.clientOpts) === null || _this$clientOpts7 === void 0 ? void 0 : _this$clientOpts7.experimentalThreadSupport) || false;
  }

  /**
   * A helper to determine thread support
   * @returns a boolean to determine if threads are enabled
   */
  supportsThreads() {
    var _this$clientOpts8;
    return ((_this$clientOpts8 = this.clientOpts) === null || _this$clientOpts8 === void 0 ? void 0 : _this$clientOpts8.threadSupport) || false;
  }

  /**
   * A helper to determine intentional mentions support
   * @returns a boolean to determine if intentional mentions are enabled
   * @experimental
   */
  supportsIntentionalMentions() {
    var _this$clientOpts9;
    return ((_this$clientOpts9 = this.clientOpts) === null || _this$clientOpts9 === void 0 ? void 0 : _this$clientOpts9.intentionalMentions) || false;
  }

  /**
   * Fetches the summary of a room as defined by an initial version of MSC3266 and implemented in Synapse
   * Proposed at https://github.com/matrix-org/matrix-doc/pull/3266
   * @param roomIdOrAlias - The ID or alias of the room to get the summary of.
   * @param via - The list of servers which know about the room if only an ID was provided.
   */
  async getRoomSummary(roomIdOrAlias, via) {
    const path = utils.encodeUri("/rooms/$roomid/summary", {
      $roomid: roomIdOrAlias
    });
    return this.http.authedRequest(_httpApi.Method.Get, path, {
      via
    }, undefined, {
      prefix: "/_matrix/client/unstable/im.nheko.summary"
    });
  }

  /**
   * Processes a list of threaded events and adds them to their respective timelines
   * @param room - the room the adds the threaded events
   * @param threadedEvents - an array of the threaded events
   * @param toStartOfTimeline - the direction in which we want to add the events
   */
  processThreadEvents(room, threadedEvents, toStartOfTimeline) {
    room.processThreadedEvents(threadedEvents, toStartOfTimeline);
  }

  /**
   * Processes a list of thread roots and creates a thread model
   * @param room - the room to create the threads in
   * @param threadedEvents - an array of thread roots
   * @param toStartOfTimeline - the direction
   */
  processThreadRoots(room, threadedEvents, toStartOfTimeline) {
    room.processThreadRoots(threadedEvents, toStartOfTimeline);
  }
  processBeaconEvents(room, events) {
    this.processAggregatedTimelineEvents(room, events);
  }

  /**
   * Calls aggregation functions for event types that are aggregated
   * Polls and location beacons
   * @param room - room the events belong to
   * @param events - timeline events to be processed
   * @returns
   */
  processAggregatedTimelineEvents(room, events) {
    if (!(events !== null && events !== void 0 && events.length)) return;
    if (!room) return;
    room.currentState.processBeaconEvents(events, this);
    room.processPollEvents(events);
  }

  /**
   * Fetches information about the user for the configured access token.
   */
  async whoami() {
    return this.http.authedRequest(_httpApi.Method.Get, "/account/whoami");
  }

  /**
   * Find the event_id closest to the given timestamp in the given direction.
   * @returns Resolves: A promise of an object containing the event_id and
   *    origin_server_ts of the closest event to the timestamp in the given direction
   * @returns Rejects: when the request fails (module:http-api.MatrixError)
   */
  async timestampToEvent(roomId, timestamp, dir) {
    const path = utils.encodeUri("/rooms/$roomId/timestamp_to_event", {
      $roomId: roomId
    });
    const queryParams = {
      ts: timestamp.toString(),
      dir: dir
    };
    try {
      return await this.http.authedRequest(_httpApi.Method.Get, path, queryParams, undefined, {
        prefix: _httpApi.ClientPrefix.V1
      });
    } catch (err) {
      // Fallback to the prefixed unstable endpoint. Since the stable endpoint is
      // new, we should also try the unstable endpoint before giving up. We can
      // remove this fallback request in a year (remove after 2023-11-28).
      if (err.errcode === "M_UNRECOGNIZED" && (
      // XXX: The 400 status code check should be removed in the future
      // when Synapse is compliant with MSC3743.
      err.httpStatus === 400 ||
      // This the correct standard status code for an unsupported
      // endpoint according to MSC3743. Not Found and Method Not Allowed
      // both indicate that this endpoint+verb combination is
      // not supported.
      err.httpStatus === 404 || err.httpStatus === 405)) {
        return await this.http.authedRequest(_httpApi.Method.Get, path, queryParams, undefined, {
          prefix: "/_matrix/client/unstable/org.matrix.msc3030"
        });
      }
      throw err;
    }
  }
}

/**
 * recalculates an accurate notifications count on event decryption.
 * Servers do not have enough knowledge about encrypted events to calculate an
 * accurate notification_count
 */
exports.MatrixClient = MatrixClient;
(0, _defineProperty2.default)(MatrixClient, "RESTORE_BACKUP_ERROR_BAD_KEY", "RESTORE_BACKUP_ERROR_BAD_KEY");
function fixNotificationCountOnDecryption(cli, event) {
  var _oldActions$tweaks, _actions$tweaks;
  const ourUserId = cli.getUserId();
  const eventId = event.getId();
  const room = cli.getRoom(event.getRoomId());
  if (!room || !ourUserId || !eventId) return;
  const oldActions = event.getPushActions();
  const actions = cli.getPushActionsForEvent(event, true);
  const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;
  const currentHighlightCount = room.getUnreadCountForEventContext(_room.NotificationCountType.Highlight, event);

  // 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);
  let hasReadEvent;
  if (isThreadEvent) {
    const thread = room.getThread(event.threadRootId);
    hasReadEvent = thread ? thread.hasUserReadEvent(ourUserId, eventId) :
    // If the thread object does not exist in the room yet, we don't
    // want to calculate notification for this event yet. We have not
    // restored the read receipts yet and can't accurately calculate
    // notifications at this stage.
    //
    // This issue can likely go away when MSC3874 is implemented
    true;
  } else {
    hasReadEvent = room.hasUserReadEvent(ourUserId, eventId);
  }
  if (hasReadEvent) {
    // If the event has been read, ignore it.
    return;
  }
  if (oldHighlight !== newHighlight || currentHighlightCount > 0) {
    // TODO: Handle mentions received while the client is offline
    // See also https://github.com/vector-im/element-web/issues/9069
    let newCount = currentHighlightCount;
    if (newHighlight && !oldHighlight) newCount++;
    if (!newHighlight && oldHighlight) newCount--;
    if (isThreadEvent) {
      room.setThreadUnreadNotificationCount(event.threadRootId, _room.NotificationCountType.Highlight, newCount);
    } else {
      room.setUnreadNotificationCount(_room.NotificationCountType.Highlight, newCount);
    }
  }

  // Total count is used to typically increment a room notification counter, but not loudly highlight it.
  const currentTotalCount = room.getUnreadCountForEventContext(_room.NotificationCountType.Total, event);

  // `notify` is used in practice for incrementing the total count
  const newNotify = !!(actions !== null && actions !== void 0 && actions.notify);

  // The room total count is NEVER incremented by the server for encrypted rooms. We basically ignore
  // the server here as it's always going to tell us to increment for encrypted events.
  if (newNotify) {
    if (isThreadEvent) {
      room.setThreadUnreadNotificationCount(event.threadRootId, _room.NotificationCountType.Total, currentTotalCount + 1);
    } else {
      room.setUnreadNotificationCount(_room.NotificationCountType.Total, currentTotalCount + 1);
    }
  }
}
//# sourceMappingURL=client.js.map