Bug 1533872 - Update the matrix-js-sdk to v2.4.1. rs=florian
authorPatrick Cloke <clokep@gmail.com>
Tue, 22 Oct 2019 12:07:09 -0400
changeset 36447 d296be7570aebc76856d3539c51a698ce199b1ef
parent 36446 64c4bc07ed19f7f358fc2e8a8805977d6c0d73e8
child 36448 d31d845ea41e096fba0c805591e1a122c5c319ed
push id2534
push userclokep@gmail.com
push dateMon, 02 Dec 2019 19:52:51 +0000
treeherdercomm-beta@055c50840778 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflorian
bugs1533872
Bug 1533872 - Update the matrix-js-sdk to v2.4.1. rs=florian
chat/protocols/matrix/lib/matrix-sdk/LICENSE
chat/protocols/matrix/lib/matrix-sdk/ReEmitter.js
chat/protocols/matrix/lib/matrix-sdk/autodiscovery.js
chat/protocols/matrix/lib/matrix-sdk/base-apis.js
chat/protocols/matrix/lib/matrix-sdk/client.js
chat/protocols/matrix/lib/matrix-sdk/content-helpers.js
chat/protocols/matrix/lib/matrix-sdk/content-repo.js
chat/protocols/matrix/lib/matrix-sdk/crypto/DeviceList.js
chat/protocols/matrix/lib/matrix-sdk/crypto/OlmDevice.js
chat/protocols/matrix/lib/matrix-sdk/crypto/OutgoingRoomKeyRequestManager.js
chat/protocols/matrix/lib/matrix-sdk/crypto/RoomList.js
chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/base.js
chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/index.js
chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/megolm.js
chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/olm.js
chat/protocols/matrix/lib/matrix-sdk/crypto/backup_password.js
chat/protocols/matrix/lib/matrix-sdk/crypto/deviceinfo.js
chat/protocols/matrix/lib/matrix-sdk/crypto/index.js
chat/protocols/matrix/lib/matrix-sdk/crypto/olmlib.js
chat/protocols/matrix/lib/matrix-sdk/crypto/recoverykey.js
chat/protocols/matrix/lib/matrix-sdk/crypto/store/base.js
chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store-backend.js
chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store.js
chat/protocols/matrix/lib/matrix-sdk/crypto/store/localStorage-crypto-store.js
chat/protocols/matrix/lib/matrix-sdk/crypto/store/memory-crypto-store.js
chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Base.js
chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Error.js
chat/protocols/matrix/lib/matrix-sdk/crypto/verification/QRCode.js
chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SAS.js
chat/protocols/matrix/lib/matrix-sdk/errors.js
chat/protocols/matrix/lib/matrix-sdk/filter-component.js
chat/protocols/matrix/lib/matrix-sdk/filter.js
chat/protocols/matrix/lib/matrix-sdk/http-api.js
chat/protocols/matrix/lib/matrix-sdk/indexeddb-helpers.js
chat/protocols/matrix/lib/matrix-sdk/indexeddb-worker.js
chat/protocols/matrix/lib/matrix-sdk/interactive-auth.js
chat/protocols/matrix/lib/matrix-sdk/logger.js
chat/protocols/matrix/lib/matrix-sdk/matrix.js
chat/protocols/matrix/lib/matrix-sdk/models/event-context.js
chat/protocols/matrix/lib/matrix-sdk/models/event-timeline-set.js
chat/protocols/matrix/lib/matrix-sdk/models/event-timeline.js
chat/protocols/matrix/lib/matrix-sdk/models/event.js
chat/protocols/matrix/lib/matrix-sdk/models/group.js
chat/protocols/matrix/lib/matrix-sdk/models/relations.js
chat/protocols/matrix/lib/matrix-sdk/models/room-member.js
chat/protocols/matrix/lib/matrix-sdk/models/room-state.js
chat/protocols/matrix/lib/matrix-sdk/models/room-summary.js
chat/protocols/matrix/lib/matrix-sdk/models/room.js
chat/protocols/matrix/lib/matrix-sdk/models/search-result.js
chat/protocols/matrix/lib/matrix-sdk/models/user.js
chat/protocols/matrix/lib/matrix-sdk/pushprocessor.js
chat/protocols/matrix/lib/matrix-sdk/randomstring.js
chat/protocols/matrix/lib/matrix-sdk/realtime-callbacks.js
chat/protocols/matrix/lib/matrix-sdk/scheduler.js
chat/protocols/matrix/lib/matrix-sdk/service-types.js
chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-local-backend.js
chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-remote-backend.js
chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-store-worker.js
chat/protocols/matrix/lib/matrix-sdk/store/indexeddb.js
chat/protocols/matrix/lib/matrix-sdk/store/memory.js
chat/protocols/matrix/lib/matrix-sdk/store/session/webstorage.js
chat/protocols/matrix/lib/matrix-sdk/store/stub.js
chat/protocols/matrix/lib/matrix-sdk/store/webstorage.js
chat/protocols/matrix/lib/matrix-sdk/sync-accumulator.js
chat/protocols/matrix/lib/matrix-sdk/sync.js
chat/protocols/matrix/lib/matrix-sdk/timeline-window.js
chat/protocols/matrix/lib/matrix-sdk/utils.js
chat/protocols/matrix/lib/matrix-sdk/webrtc/call.js
chat/protocols/matrix/lib/moz.build
chat/protocols/matrix/matrix-sdk.jsm
new file mode 100644
--- /dev/null
+++ b/chat/protocols/matrix/lib/matrix-sdk/LICENSE
@@ -0,0 +1,177 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
new file mode 100644
--- /dev/null
+++ b/chat/protocols/matrix/lib/matrix-sdk/ReEmitter.js
@@ -0,0 +1,58 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+    value: true
+});
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2017 Vector Creations Ltd
+Copyright 2017 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/**
+ * @module
+ */
+
+class Reemitter {
+    constructor(target) {
+        this.target = target;
+
+        // We keep one bound event handler for each event name so we know
+        // what event is arriving
+        this.boundHandlers = {};
+    }
+
+    _handleEvent(eventName, ...args) {
+        this.target.emit(eventName, ...args);
+    }
+
+    reEmit(source, eventNames) {
+        // We include the source as the last argument for event handlers which may need it,
+        // such as read receipt listeners on the client class which won't have the context
+        // of the room.
+        const forSource = (handler, ...args) => {
+            handler(...args, source);
+        };
+        for (const eventName of eventNames) {
+            if (this.boundHandlers[eventName] === undefined) {
+                this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName);
+            }
+
+            const boundHandler = forSource.bind(this, this.boundHandlers[eventName]);
+            source.on(eventName, boundHandler);
+        }
+    }
+}
+exports.default = Reemitter;
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/chat/protocols/matrix/lib/matrix-sdk/autodiscovery.js
@@ -0,0 +1,540 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+    value: true
+});
+exports.AutoDiscovery = undefined;
+
+var _bluebird = require('bluebird');
+
+var _bluebird2 = _interopRequireDefault(_bluebird);
+
+var _logger = require('./logger');
+
+var _logger2 = _interopRequireDefault(_logger);
+
+var _url = require('url');
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+// Dev note: Auto discovery is part of the spec.
+// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
+
+/**
+ * Description for what an automatically discovered client configuration
+ * would look like. Although this is a class, it is recommended that it
+ * be treated as an interface definition rather than as a class.
+ *
+ * Additional properties than those defined here may be present, and
+ * should follow the Java package naming convention.
+ */
+class DiscoveredClientConfig {
+    // eslint-disable-line no-unused-vars
+    // Dev note: this is basically a copy/paste of the .well-known response
+    // object as defined in the spec. It does have additional information,
+    // however. Overall, this exists to serve as a place for documentation
+    // and not functionality.
+    // See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client
+
+    constructor() {
+        /**
+         * The homeserver configuration the client should use. This will
+         * always be present on the object.
+         * @type {{state: string, base_url: string}} The configuration.
+         */
+        this["m.homeserver"] = {
+            /**
+             * The lookup result state. If this is anything other than
+             * AutoDiscovery.SUCCESS then base_url may be falsey. Additionally,
+             * if this is not AutoDiscovery.SUCCESS then the client should
+             * assume the other properties in the client config (such as
+             * the identity server configuration) are not valid.
+             */
+            state: AutoDiscovery.PROMPT,
+
+            /**
+             * If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT
+             * then this will contain a human-readable (English) message
+             * for what went wrong. If the state is none of those previously
+             * mentioned, this will be falsey.
+             */
+            error: "Something went wrong",
+
+            /**
+             * The base URL clients should use to talk to the homeserver,
+             * particularly for the login process. May be falsey if the
+             * state is not AutoDiscovery.SUCCESS.
+             */
+            base_url: "https://matrix.org"
+        };
+
+        /**
+         * The identity server configuration the client should use. This
+         * will always be present on teh object.
+         * @type {{state: string, base_url: string}} The configuration.
+         */
+        this["m.identity_server"] = {
+            /**
+             * The lookup result state. If this is anything other than
+             * AutoDiscovery.SUCCESS then base_url may be falsey.
+             */
+            state: AutoDiscovery.PROMPT,
+
+            /**
+             * The base URL clients should use for interacting with the
+             * identity server. May be falsey if the state is not
+             * AutoDiscovery.SUCCESS.
+             */
+            base_url: "https://vector.im"
+        };
+    }
+}
+
+/**
+ * Utilities for automatically discovery resources, such as homeservers
+ * for users to log in to.
+ */
+/*
+Copyright 2018 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/** @module auto-discovery */
+
+class AutoDiscovery {
+    // Dev note: the constants defined here are related to but not
+    // exactly the same as those in the spec. This is to hopefully
+    // translate the meaning of the states in the spec, but also
+    // support our own if needed.
+
+    static get ERROR_INVALID() {
+        return "Invalid homeserver discovery response";
+    }
+
+    static get ERROR_GENERIC_FAILURE() {
+        return "Failed to get autodiscovery configuration from server";
+    }
+
+    static get ERROR_INVALID_HS_BASE_URL() {
+        return "Invalid base_url for m.homeserver";
+    }
+
+    static get ERROR_INVALID_HOMESERVER() {
+        return "Homeserver URL does not appear to be a valid Matrix homeserver";
+    }
+
+    static get ERROR_INVALID_IS_BASE_URL() {
+        return "Invalid base_url for m.identity_server";
+    }
+
+    static get ERROR_INVALID_IDENTITY_SERVER() {
+        return "Identity server URL does not appear to be a valid identity server";
+    }
+
+    static get ERROR_INVALID_IS() {
+        return "Invalid identity server discovery response";
+    }
+
+    static get ERROR_MISSING_WELLKNOWN() {
+        return "No .well-known JSON file found";
+    }
+
+    static get ERROR_INVALID_JSON() {
+        return "Invalid JSON";
+    }
+
+    static get ALL_ERRORS() {
+        return [AutoDiscovery.ERROR_INVALID, AutoDiscovery.ERROR_GENERIC_FAILURE, AutoDiscovery.ERROR_INVALID_HS_BASE_URL, AutoDiscovery.ERROR_INVALID_HOMESERVER, AutoDiscovery.ERROR_INVALID_IS_BASE_URL, AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER, AutoDiscovery.ERROR_INVALID_IS, AutoDiscovery.ERROR_MISSING_WELLKNOWN, AutoDiscovery.ERROR_INVALID_JSON];
+    }
+
+    /**
+     * The auto discovery failed. The client is expected to communicate
+     * the error to the user and refuse logging in.
+     * @return {string}
+     * @constructor
+     */
+    static get FAIL_ERROR() {
+        return "FAIL_ERROR";
+    }
+
+    /**
+     * The auto discovery failed, however the client may still recover
+     * from the problem. The client is recommended to that the same
+     * action it would for PROMPT while also warning the user about
+     * what went wrong. The client may also treat this the same as
+     * a FAIL_ERROR state.
+     * @return {string}
+     * @constructor
+     */
+    static get FAIL_PROMPT() {
+        return "FAIL_PROMPT";
+    }
+
+    /**
+     * The auto discovery didn't fail but did not find anything of
+     * interest. The client is expected to prompt the user for more
+     * information, or fail if it prefers.
+     * @return {string}
+     * @constructor
+     */
+    static get PROMPT() {
+        return "PROMPT";
+    }
+
+    /**
+     * The auto discovery was successful.
+     * @return {string}
+     * @constructor
+     */
+    static get SUCCESS() {
+        return "SUCCESS";
+    }
+
+    /**
+     * Validates and verifies client configuration information for purposes
+     * of logging in. Such information includes the homeserver URL
+     * and identity server URL the client would want. Additional details
+     * may also be included, and will be transparently brought into the
+     * response object unaltered.
+     * @param {string} wellknown The configuration object itself, as returned
+     * by the .well-known auto-discovery endpoint.
+     * @return {Promise<DiscoveredClientConfig>} Resolves to the verified
+     * configuration, which may include error states. Rejects on unexpected
+     * failure, not when verification fails.
+     */
+    static async fromDiscoveryConfig(wellknown) {
+        // Step 1 is to get the config, which is provided to us here.
+
+        // We default to an error state to make the first few checks easier to
+        // write. We'll update the properties of this object over the duration
+        // of this function.
+        const clientConfig = {
+            "m.homeserver": {
+                state: AutoDiscovery.FAIL_ERROR,
+                error: AutoDiscovery.ERROR_INVALID,
+                base_url: null
+            },
+            "m.identity_server": {
+                // Technically, we don't have a problem with the identity server
+                // config at this point.
+                state: AutoDiscovery.PROMPT,
+                error: null,
+                base_url: null
+            }
+        };
+
+        if (!wellknown || !wellknown["m.homeserver"]) {
+            _logger2.default.error("No m.homeserver key in config");
+
+            clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
+            clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
+
+            return _bluebird2.default.resolve(clientConfig);
+        }
+
+        if (!wellknown["m.homeserver"]["base_url"]) {
+            _logger2.default.error("No m.homeserver base_url in config");
+
+            clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
+            clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
+
+            return _bluebird2.default.resolve(clientConfig);
+        }
+
+        // Step 2: Make sure the homeserver URL is valid *looking*. We'll make
+        // sure it points to a homeserver in Step 3.
+        const hsUrl = this._sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]);
+        if (!hsUrl) {
+            _logger2.default.error("Invalid base_url for m.homeserver");
+            clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
+            return _bluebird2.default.resolve(clientConfig);
+        }
+
+        // Step 3: Make sure the homeserver URL points to a homeserver.
+        const hsVersions = await this._fetchWellKnownObject(`${hsUrl}/_matrix/client/versions`);
+        if (!hsVersions || !hsVersions.raw["versions"]) {
+            _logger2.default.error("Invalid /versions response");
+            clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
+
+            // Supply the base_url to the caller because they may be ignoring liveliness
+            // errors, like this one.
+            clientConfig["m.homeserver"].base_url = hsUrl;
+
+            return _bluebird2.default.resolve(clientConfig);
+        }
+
+        // Step 4: Now that the homeserver looks valid, update our client config.
+        clientConfig["m.homeserver"] = {
+            state: AutoDiscovery.SUCCESS,
+            error: null,
+            base_url: hsUrl
+        };
+
+        // Step 5: Try to pull out the identity server configuration
+        let isUrl = "";
+        if (wellknown["m.identity_server"]) {
+            // We prepare a failing identity server response to save lines later
+            // in this branch. Note that we also fail the homeserver check in the
+            // object because according to the spec we're supposed to FAIL_ERROR
+            // if *anything* goes wrong with the IS validation, including invalid
+            // format. This means we're supposed to stop discovery completely.
+            const failingClientConfig = {
+                "m.homeserver": {
+                    state: AutoDiscovery.FAIL_ERROR,
+                    error: AutoDiscovery.ERROR_INVALID_IS,
+
+                    // We'll provide the base_url that was previously valid for
+                    // debugging purposes.
+                    base_url: clientConfig["m.homeserver"].base_url
+                },
+                "m.identity_server": {
+                    state: AutoDiscovery.FAIL_ERROR,
+                    error: AutoDiscovery.ERROR_INVALID_IS,
+                    base_url: null
+                }
+            };
+
+            // Step 5a: Make sure the URL is valid *looking*. We'll make sure it
+            // points to an identity server in Step 5b.
+            isUrl = this._sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]);
+            if (!isUrl) {
+                _logger2.default.error("Invalid base_url for m.identity_server");
+                failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IS_BASE_URL;
+                return _bluebird2.default.resolve(failingClientConfig);
+            }
+
+            // Step 5b: Verify there is an identity server listening on the provided
+            // URL.
+            const isResponse = await this._fetchWellKnownObject(`${isUrl}/_matrix/identity/api/v1`);
+            if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
+                _logger2.default.error("Invalid /api/v1 response");
+                failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
+
+                // Supply the base_url to the caller because they may be ignoring
+                // liveliness errors, like this one.
+                failingClientConfig["m.identity_server"].base_url = isUrl;
+
+                return _bluebird2.default.resolve(failingClientConfig);
+            }
+        }
+
+        // Step 6: Now that the identity server is valid, or never existed,
+        // populate the IS section.
+        if (isUrl && isUrl.length > 0) {
+            clientConfig["m.identity_server"] = {
+                state: AutoDiscovery.SUCCESS,
+                error: null,
+                base_url: isUrl
+            };
+        }
+
+        // Step 7: Copy any other keys directly into the clientConfig. This is for
+        // things like custom configuration of services.
+        Object.keys(wellknown).map(k => {
+            if (k === "m.homeserver" || k === "m.identity_server") {
+                // Only copy selected parts of the config to avoid overwriting
+                // properties computed by the validation logic above.
+                const notProps = ["error", "state", "base_url"];
+                for (const prop of Object.keys(wellknown[k])) {
+                    if (notProps.includes(prop)) continue;
+                    clientConfig[k][prop] = wellknown[k][prop];
+                }
+            } else {
+                // Just copy the whole thing over otherwise
+                clientConfig[k] = wellknown[k];
+            }
+        });
+
+        // Step 8: Give the config to the caller (finally)
+        return _bluebird2.default.resolve(clientConfig);
+    }
+
+    /**
+     * Attempts to automatically discover client configuration information
+     * prior to logging in. Such information includes the homeserver URL
+     * and identity server URL the client would want. Additional details
+     * may also be discovered, and will be transparently included in the
+     * response object unaltered.
+     * @param {string} domain The homeserver domain to perform discovery
+     * on. For example, "matrix.org".
+     * @return {Promise<DiscoveredClientConfig>} Resolves to the discovered
+     * configuration, which may include error states. Rejects on unexpected
+     * failure, not when discovery fails.
+     */
+    static async findClientConfig(domain) {
+        if (!domain || typeof domain !== "string" || domain.length === 0) {
+            throw new Error("'domain' must be a string of non-zero length");
+        }
+
+        // We use a .well-known lookup for all cases. According to the spec, we
+        // can do other discovery mechanisms if we want such as custom lookups
+        // however we won't bother with that here (mostly because the spec only
+        // supports .well-known right now).
+        //
+        // By using .well-known, we need to ensure we at least pull out a URL
+        // for the homeserver. We don't really need an identity server configuration
+        // but will return one anyways (with state PROMPT) to make development
+        // easier for clients. If we can't get a homeserver URL, all bets are
+        // off on the rest of the config and we'll assume it is invalid too.
+
+        // We default to an error state to make the first few checks easier to
+        // write. We'll update the properties of this object over the duration
+        // of this function.
+        const clientConfig = {
+            "m.homeserver": {
+                state: AutoDiscovery.FAIL_ERROR,
+                error: AutoDiscovery.ERROR_INVALID,
+                base_url: null
+            },
+            "m.identity_server": {
+                // Technically, we don't have a problem with the identity server
+                // config at this point.
+                state: AutoDiscovery.PROMPT,
+                error: null,
+                base_url: null
+            }
+        };
+
+        // Step 1: Actually request the .well-known JSON file and make sure it
+        // at least has a homeserver definition.
+        const wellknown = await this._fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`);
+        if (!wellknown || wellknown.action !== "SUCCESS") {
+            _logger2.default.error("No response or error when parsing .well-known");
+            if (wellknown.reason) _logger2.default.error(wellknown.reason);
+            if (wellknown.action === "IGNORE") {
+                clientConfig["m.homeserver"] = {
+                    state: AutoDiscovery.PROMPT,
+                    error: null,
+                    base_url: null
+                };
+            } else {
+                // this can only ever be FAIL_PROMPT at this point.
+                clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
+                clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
+            }
+            return _bluebird2.default.resolve(clientConfig);
+        }
+
+        // Step 2: Validate and parse the config
+        return AutoDiscovery.fromDiscoveryConfig(wellknown.raw);
+    }
+
+    /**
+     * Gets the raw discovery client configuration for the given domain name.
+     * Should only be used if there's no validation to be done on the resulting
+     * object, otherwise use findClientConfig().
+     * @param {string} domain The domain to get the client config for.
+     * @returns {Promise<object>} Resolves to the domain's client config. Can
+     * be an empty object.
+     */
+    static async getRawClientConfig(domain) {
+        if (!domain || typeof domain !== "string" || domain.length === 0) {
+            throw new Error("'domain' must be a string of non-zero length");
+        }
+
+        const response = await this._fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`);
+        if (!response) return {};
+        return response.raw || {};
+    }
+
+    /**
+     * Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
+     * is suitable for the requirements laid out by .well-known auto discovery.
+     * If valid, the URL will also be stripped of any trailing slashes.
+     * @param {string} url The potentially invalid URL to sanitize.
+     * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
+     * @private
+     */
+    static _sanitizeWellKnownUrl(url) {
+        if (!url) return false;
+
+        try {
+            // We have to try and parse the URL using the NodeJS URL
+            // library if we're on NodeJS and use the browser's URL
+            // library when we're in a browser. To accomplish this, we
+            // try the NodeJS version first and fall back to the browser.
+            let parsed = null;
+            try {
+                if (_url.URL) parsed = new _url.URL(url);else parsed = new URL(url);
+            } catch (e) {
+                parsed = new URL(url);
+            }
+
+            if (!parsed || !parsed.hostname) return false;
+            if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
+
+            const port = parsed.port ? `:${parsed.port}` : "";
+            const path = parsed.pathname ? parsed.pathname : "";
+            let saferUrl = `${parsed.protocol}//${parsed.hostname}${port}${path}`;
+            if (saferUrl.endsWith("/")) {
+                saferUrl = saferUrl.substring(0, saferUrl.length - 1);
+            }
+            return saferUrl;
+        } catch (e) {
+            _logger2.default.error(e);
+            return false;
+        }
+    }
+
+    /**
+     * Fetches a JSON object from a given URL, as expected by all .well-known
+     * related lookups. If the server gives a 404 then the `action` will be
+     * IGNORE. If the server returns something that isn't JSON, the `action`
+     * will be FAIL_PROMPT. For any other failure the `action` will be FAIL_PROMPT.
+     *
+     * The returned object will be a result of the call in object form with
+     * the following properties:
+     *   raw: The JSON object returned by the server.
+     *   action: One of SUCCESS, IGNORE, or FAIL_PROMPT.
+     *   reason: Relatively human readable description of what went wrong.
+     *   error: The actual Error, if one exists.
+     * @param {string} url The URL to fetch a JSON object from.
+     * @return {Promise<object>} Resolves to the returned state.
+     * @private
+     */
+    static async _fetchWellKnownObject(url) {
+        return new _bluebird2.default(function (resolve, reject) {
+            const request = require("./matrix").getRequest();
+            if (!request) throw new Error("No request library available");
+            request({ method: "GET", uri: url, timeout: 5000 }, (err, response, body) => {
+                if (err || response.statusCode < 200 || response.statusCode >= 300) {
+                    let action = "FAIL_PROMPT";
+                    let reason = (err ? err.message : null) || "General failure";
+                    if (response.statusCode === 404) {
+                        action = "IGNORE";
+                        reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
+                    }
+                    resolve({ raw: {}, action: action, reason: reason, error: err });
+                    return;
+                }
+
+                try {
+                    resolve({ raw: JSON.parse(body), action: "SUCCESS" });
+                } catch (e) {
+                    let reason = AutoDiscovery.ERROR_INVALID;
+                    if (e.name === "SyntaxError") {
+                        reason = AutoDiscovery.ERROR_INVALID_JSON;
+                    }
+                    resolve({
+                        raw: {},
+                        action: "FAIL_PROMPT",
+                        reason: reason,
+                        error: e
+                    });
+                }
+            });
+        });
+    }
+}
+exports.AutoDiscovery = AutoDiscovery;
\ No newline at end of file
--- a/chat/protocols/matrix/lib/matrix-sdk/base-apis.js
+++ b/chat/protocols/matrix/lib/matrix-sdk/base-apis.js
@@ -1,10 +1,13 @@
 /*
 Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2017 Vector Creations Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
     http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
@@ -17,18 +20,38 @@ limitations under the License.
 
 /**
  * This is an internal module. MatrixBaseApis is currently only meant to be used
  * by {@link client~MatrixClient}.
  *
  * @module base-apis
  */
 
-var httpApi = require("./http-api");
-var utils = require("./utils");
+var _serviceTypes = require('./service-types');
+
+var _logger = require('./logger');
+
+var _logger2 = _interopRequireDefault(_logger);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const httpApi = require("./http-api");
+const utils = require("./utils");
+const PushProcessor = require("./pushprocessor");
+
+function termsUrlForService(serviceType, baseUrl) {
+    switch (serviceType) {
+        case _serviceTypes.SERVICE_TYPES.IS:
+            return baseUrl + httpApi.PREFIX_IDENTITY_V2 + '/terms';
+        case _serviceTypes.SERVICE_TYPES.IM:
+            return baseUrl + '/_matrix/integrations/v1/terms';
+        default:
+            throw new Error('Unsupported service type');
+    }
+}
 
 /**
  * Low-level wrappers for the Matrix APIs
  *
  * @constructor
  *
  * @param {Object} opts Configuration options
  *
@@ -40,278 +63,381 @@ var utils = require("./utils");
  *
  * @param {Function} opts.request Required. The function to invoke for HTTP
  * requests. The value of this property is typically <code>require("request")
  * </code> as it returns a function which meets the required interface. See
  * {@link requestFunction} for more information.
  *
  * @param {string} opts.accessToken The access_token for this user.
  *
+ * @param {IdentityServerProvider} [opts.identityServer]
+ * Optional. A provider object with one function `getAccessToken`, which is a
+ * callback that returns a Promise<String> of an identity access token to supply
+ * with identity requests. If the object is unset, no access token will be
+ * supplied.
+ * See also https://github.com/vector-im/riot-web/issues/10615 which seeks to
+ * replace the previous approach of manual access tokens params with this
+ * callback throughout the SDK.
+ *
+ * @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of
+ * time to wait before timing out HTTP requests. If not specified, there is no
+ * timeout.
+ *
  * @param {Object} opts.queryParams Optional. Extra query parameters to append
  * to all requests with this client. Useful for application services which require
  * <code>?user_id=</code>.
  *
+ * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
+ * Authorization header instead of query param to send the access token to the server.
  */
 function MatrixBaseApis(opts) {
     utils.checkObjectHasKeys(opts, ["baseUrl", "request"]);
 
     this.baseUrl = opts.baseUrl;
     this.idBaseUrl = opts.idBaseUrl;
+    this.identityServer = opts.identityServer;
 
-    var httpOpts = {
+    const httpOpts = {
         baseUrl: opts.baseUrl,
         idBaseUrl: opts.idBaseUrl,
         accessToken: opts.accessToken,
         request: opts.request,
         prefix: httpApi.PREFIX_R0,
         onlyData: true,
-        extraParams: opts.queryParams
+        extraParams: opts.queryParams,
+        localTimeoutMs: opts.localTimeoutMs,
+        useAuthorizationHeader: opts.useAuthorizationHeader
     };
     this._http = new httpApi.MatrixHttpApi(this, httpOpts);
 
     this._txnCtr = 0;
 }
 
 /**
  * Get the Homeserver URL of this client
  * @return {string} Homeserver URL of this client
  */
-MatrixBaseApis.prototype.getHomeserverUrl = function() {
+MatrixBaseApis.prototype.getHomeserverUrl = function () {
     return this.baseUrl;
 };
 
 /**
  * Get the Identity Server URL of this client
+ * @param {boolean} stripProto whether or not to strip the protocol from the URL
  * @return {string} Identity Server URL of this client
  */
-MatrixBaseApis.prototype.getIdentityServerUrl = function() {
+MatrixBaseApis.prototype.getIdentityServerUrl = function (stripProto = false) {
+    if (stripProto && (this.idBaseUrl.startsWith("http://") || this.idBaseUrl.startsWith("https://"))) {
+        return this.idBaseUrl.split("://")[1];
+    }
     return this.idBaseUrl;
 };
 
 /**
+ * Set the Identity Server URL of this client
+ * @param {string} url New Identity Server URL
+ */
+MatrixBaseApis.prototype.setIdentityServerUrl = function (url) {
+    this.idBaseUrl = utils.ensureNoTrailingSlash(url);
+    this._http.setIdBaseUrl(this.idBaseUrl);
+};
+
+/**
  * Get the access token associated with this account.
  * @return {?String} The access_token or null
  */
-MatrixBaseApis.prototype.getAccessToken = function() {
+MatrixBaseApis.prototype.getAccessToken = function () {
     return this._http.opts.accessToken || null;
 };
 
 /**
  * @return {boolean} true if there is a valid access_token for this client.
  */
-MatrixBaseApis.prototype.isLoggedIn = function() {
+MatrixBaseApis.prototype.isLoggedIn = function () {
     return this._http.opts.accessToken !== undefined;
 };
 
 /**
  * Make up a new transaction id
  *
  * @return {string} a new, unique, transaction id
  */
-MatrixBaseApis.prototype.makeTxnId = function() {
-    return "m" + new Date().getTime() + "." + (this._txnCtr++);
+MatrixBaseApis.prototype.makeTxnId = function () {
+    return "m" + new Date().getTime() + "." + this._txnCtr++;
 };
 
-
 // Registration/Login operations
 // =============================
 
 /**
+ * Check whether a username is available prior to registration. An error response
+ * indicates an invalid/unavailable username.
+ * @param {string} username The username to check the availability of.
+ * @return {module:client.Promise} Resolves: to `true`.
+ */
+MatrixBaseApis.prototype.isUsernameAvailable = function (username) {
+    return this._http.authedRequest(undefined, "GET", '/register/available', { username: username }).then(response => {
+        return response.available;
+    });
+};
+
+/**
  * @param {string} username
  * @param {string} password
  * @param {string} sessionId
  * @param {Object} auth
- * @param {boolean} bindEmail
+ * @param {Object} bindThreepids Set key 'email' to true to bind any email
+ *     threepid uses during registration in the ID server. Set 'msisdn' to
+ *     true to bind msisdn.
  * @param {string} guestAccessToken
+ * @param {string} inhibitLogin
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.register = function(
-    username, password,
-    sessionId, auth, bindEmail, guestAccessToken,
-    callback
-) {
-    if (auth === undefined) { auth = {}; }
-    if (sessionId) { auth.session = sessionId; }
+MatrixBaseApis.prototype.register = function (username, password, sessionId, auth, bindThreepids, guestAccessToken, inhibitLogin, callback) {
+    // backwards compat
+    if (bindThreepids === true) {
+        bindThreepids = { email: true };
+    } else if (bindThreepids === null || bindThreepids === undefined) {
+        bindThreepids = {};
+    }
+    if (typeof inhibitLogin === 'function') {
+        callback = inhibitLogin;
+        inhibitLogin = undefined;
+    }
 
-    var params = {
+    if (auth === undefined || auth === null) {
+        auth = {};
+    }
+    if (sessionId) {
+        auth.session = sessionId;
+    }
+
+    const params = {
         auth: auth
     };
-    if (username !== undefined && username !== null) { params.username = username; }
-    if (password !== undefined && password !== null) { params.password = password; }
-    if (bindEmail !== undefined && bindEmail !== null) { params.bind_email = bindEmail; }
+    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, undefined, callback);
 };
 
 /**
  * Register a guest account.
  * @param {Object=} opts Registration options
  * @param {Object} opts.body JSON HTTP body to provide.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
+MatrixBaseApis.prototype.registerGuest = function (opts, callback) {
     opts = opts || {};
     opts.body = opts.body || {};
     return this.registerRequest(opts.body, "guest", callback);
 };
 
 /**
  * @param {Object} data   parameters for registration request
  * @param {string=} kind  type of user to register. may be "guest"
  * @param {module:client.callback=} callback
  * @return {module:client.Promise} Resolves: to the /register response
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.registerRequest = function(data, kind, callback) {
-    var params = {};
-    if (kind) { params.kind = kind; }
+MatrixBaseApis.prototype.registerRequest = function (data, kind, callback) {
+    const params = {};
+    if (kind) {
+        params.kind = kind;
+    }
 
-    return this._http.request(
-        callback, "POST", "/register", params, data
-    );
+    return this._http.request(callback, "POST", "/register", params, data);
 };
 
 /**
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.loginFlows = function(callback) {
+MatrixBaseApis.prototype.loginFlows = function (callback) {
     return this._http.request(callback, "GET", "/login");
 };
 
 /**
  * @param {string} loginType
  * @param {Object} data
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.login = function(loginType, data, callback) {
-    var login_data = {
-        type: loginType,
+MatrixBaseApis.prototype.login = function (loginType, data, callback) {
+    const login_data = {
+        type: loginType
     };
 
     // merge data into login_data
     utils.extend(login_data, data);
 
-    return this._http.authedRequest(
-        callback, "POST", "/login", undefined, login_data
-    );
+    return this._http.authedRequest((error, response) => {
+        if (response && response.access_token && response.user_id) {
+            this._http.opts.accessToken = response.access_token;
+            this.credentials = {
+                userId: response.user_id
+            };
+        }
+
+        if (callback) {
+            callback(error, response);
+        }
+    }, "POST", "/login", undefined, login_data);
 };
 
 /**
  * @param {string} user
  * @param {string} password
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) {
+MatrixBaseApis.prototype.loginWithPassword = function (user, password, callback) {
     return this.login("m.login.password", {
         user: user,
         password: password
     }, callback);
 };
 
 /**
  * @param {string} relayState URL Callback after SAML2 Authentication
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.loginWithSAML2 = function(relayState, callback) {
+MatrixBaseApis.prototype.loginWithSAML2 = function (relayState, callback) {
     return this.login("m.login.saml2", {
         relay_state: relayState
     }, callback);
 };
 
 /**
  * @param {string} redirectUrl The URL to redirect to after the HS
  * authenticates with CAS.
  * @return {string} The HS URL to hit to begin the CAS login process.
  */
-MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) {
-    return this._http.getUrl("/login/cas/redirect", {
+MatrixBaseApis.prototype.getCasLoginUrl = function (redirectUrl) {
+    return this.getSsoLoginUrl(redirectUrl, "cas");
+};
+
+/**
+ * @param {string} redirectUrl The URL to redirect to after the HS
+ *     authenticates with the SSO.
+ * @param {string} loginType The type of SSO login we are doing (sso or cas).
+ *     Defaults to 'sso'.
+ * @return {string} The HS URL to hit to begin the SSO login process.
+ */
+MatrixBaseApis.prototype.getSsoLoginUrl = function (redirectUrl, loginType) {
+    if (loginType === undefined) {
+        loginType = "sso";
+    }
+    return this._http.getUrl("/login/" + loginType + "/redirect", {
         "redirectUrl": redirectUrl
-    }, httpApi.PREFIX_UNSTABLE);
+    }, httpApi.PREFIX_R0);
 };
 
 /**
  * @param {string} token Login token previously received from homeserver
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.loginWithToken = function(token, callback) {
+MatrixBaseApis.prototype.loginWithToken = function (token, callback) {
     return this.login("m.login.token", {
         token: token
     }, callback);
 };
 
-
 /**
  * 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 {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: On success, the empty object
  */
-MatrixBaseApis.prototype.logout = function(callback) {
-    return this._http.authedRequest(
-        callback, "POST", '/logout'
-    );
+MatrixBaseApis.prototype.logout = function (callback) {
+    return this._http.authedRequest(callback, "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 {object} auth Optional. Auth data to supply for User-Interactive auth.
- * @param {module:client.callback} callback Optional.
+ * @param {boolean} erase Optional. If set, send as `erase` attribute in the
+ * JSON request body, indicating whether the account should be erased. Defaults
+ * to false.
  * @return {module:client.Promise} Resolves: On success, the empty object
  */
-MatrixBaseApis.prototype.deactivateAccount = function(auth, callback) {
-    var body = {};
+MatrixBaseApis.prototype.deactivateAccount = function (auth, erase) {
+    if (typeof erase === 'function') {
+        throw new Error('deactivateAccount no longer accepts a callback parameter');
+    }
+
+    const body = {};
     if (auth) {
-        body = {
-            auth: auth,
-        };
+        body.auth = auth;
     }
-    return this._http.authedRequestWithPrefix(
-        callback, "POST", '/account/deactivate', undefined, body, httpApi.PREFIX_UNSTABLE
-    );
+    if (erase !== undefined) {
+        body.erase = erase;
+    }
+
+    return this._http.authedRequest(undefined, "POST", '/account/deactivate', undefined, body);
 };
 
 /**
  * Get the fallback URL to use for unknown interactive-auth stages.
  *
  * @param {string} loginType     the type of stage being attempted
  * @param {string} authSessionId the auth session ID provided by the homeserver
  *
  * @return {string} HS URL to hit to for the fallback interface
  */
-MatrixBaseApis.prototype.getFallbackAuthUrl = function(loginType, authSessionId) {
-    var path = utils.encodeUri("/auth/$loginType/fallback/web", {
-        $loginType: loginType,
+MatrixBaseApis.prototype.getFallbackAuthUrl = function (loginType, authSessionId) {
+    const path = utils.encodeUri("/auth/$loginType/fallback/web", {
+        $loginType: loginType
     });
 
     return this._http.getUrl(path, {
-        session: authSessionId,
+        session: authSessionId
     }, httpApi.PREFIX_R0);
 };
 
 // Room operations
 // ===============
 
 /**
  * Create a new room.
@@ -322,601 +448,1095 @@ MatrixBaseApis.prototype.getFallbackAuth
  * @param {string[]} options.invite A list of user IDs to invite to this room.
  * @param {string} options.name The name to give this room.
  * @param {string} options.topic The topic to give this room.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: <code>{room_id: {string},
  * room_alias: {string(opt)}}</code>
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.createRoom = function(options, callback) {
+MatrixBaseApis.prototype.createRoom = function (options, callback) {
     // valid options include: room_alias_name, visibility, invite
-    return this._http.authedRequest(
-        callback, "POST", "/createRoom", undefined, options
-    );
+    return this._http.authedRequest(callback, "POST", "/createRoom", undefined, options);
+};
+/**
+ * Fetches relations for a given event
+ * @param {string} roomId the room of the event
+ * @param {string} eventId the id of the event
+ * @param {string} relationType the rel_type of the relations requested
+ * @param {string} eventType the event type of the relations requested
+ * @param {Object} opts options with optional values for the request.
+ * @param {Object} opts.from the pagination token returned from a previous request as `next_batch` to return following relations.
+ * @return {Object} the response, with chunk and next_batch.
+ */
+MatrixBaseApis.prototype.fetchRelations = async function (roomId, eventId, relationType, eventType, opts) {
+    const queryParams = {};
+    if (opts.from) {
+        queryParams.from = opts.from;
+    }
+    const queryString = utils.encodeParams(queryParams);
+    const path = utils.encodeUri("/rooms/$roomId/relations/$eventId/$relationType/$eventType?" + queryString, {
+        $roomId: roomId,
+        $eventId: eventId,
+        $relationType: relationType,
+        $eventType: eventType
+    });
+    const response = await this._http.authedRequest(undefined, "GET", path, null, null, {
+        prefix: httpApi.PREFIX_UNSTABLE
+    });
+    return response;
 };
 
 /**
  * @param {string} roomId
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.roomState = function(roomId, callback) {
-    var path = utils.encodeUri("/rooms/$roomId/state", {$roomId: roomId});
+MatrixBaseApis.prototype.roomState = function (roomId, callback) {
+    const path = utils.encodeUri("/rooms/$roomId/state", { $roomId: roomId });
+    return this._http.authedRequest(callback, "GET", path);
+};
+
+/**
+ * Get an event in a room by its event id.
+ * @param {string} roomId
+ * @param {string} eventId
+ * @param {module:client.callback} callback Optional.
+ *
+ * @return {Promise} Resolves to an object containing the event.
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.fetchRoomEvent = function (roomId, eventId, callback) {
+    const path = utils.encodeUri("/rooms/$roomId/event/$eventId", {
+        $roomId: roomId,
+        $eventId: eventId
+    });
+    return this._http.authedRequest(callback, "GET", path);
+};
+
+/**
+ * @param {string} roomId
+ * @param {string} includeMembership the membership type to include in the response
+ * @param {string} excludeMembership the membership type to exclude from the response
+ * @param {string} atEventId the id of the event for which moment in the timeline the members should be returned for
+ * @param {module:client.callback} callback Optional.
+ * @return {module:client.Promise} Resolves: dictionary of userid to profile information
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.members = function (roomId, includeMembership, excludeMembership, atEventId, callback) {
+    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(callback, "GET", path);
 };
 
 /**
+ * Upgrades a room to a new protocol version
+ * @param {string} roomId
+ * @param {string} newVersion The target version to upgrade to
+ * @return {module:client.Promise} Resolves: Object with key 'replacement_room'
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.upgradeRoom = function (roomId, newVersion) {
+    const path = utils.encodeUri("/rooms/$roomId/upgrade", { $roomId: roomId });
+    return this._http.authedRequest(undefined, "POST", path, undefined, { new_version: newVersion });
+};
+
+/**
+ * @param {string} groupId
+ * @return {module:client.Promise} Resolves: Group summary object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.getGroupSummary = function (groupId) {
+    const path = utils.encodeUri("/groups/$groupId/summary", { $groupId: groupId });
+    return this._http.authedRequest(undefined, "GET", path);
+};
+
+/**
+ * @param {string} groupId
+ * @return {module:client.Promise} Resolves: Group profile object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.getGroupProfile = function (groupId) {
+    const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId });
+    return this._http.authedRequest(undefined, "GET", path);
+};
+
+/**
+ * @param {string} groupId
+ * @param {Object} profile The group profile object
+ * @param {string=} profile.name Name of the group
+ * @param {string=} profile.avatar_url MXC avatar URL
+ * @param {string=} profile.short_description A short description of the room
+ * @param {string=} profile.long_description A longer HTML description of the room
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.setGroupProfile = function (groupId, profile) {
+    const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId });
+    return this._http.authedRequest(undefined, "POST", path, undefined, profile);
+};
+
+/**
+ * @param {string} groupId
+ * @param {object} policy The join policy for the group. Must include at
+ *     least a 'type' field which is 'open' if anyone can join the group
+ *     the group without prior approval, or 'invite' if an invite is
+ *     required to join.
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.setGroupJoinPolicy = function (groupId, policy) {
+    const path = utils.encodeUri("/groups/$groupId/settings/m.join_policy", { $groupId: groupId });
+    return this._http.authedRequest(undefined, "PUT", path, undefined, {
+        'm.join_policy': policy
+    });
+};
+
+/**
+ * @param {string} groupId
+ * @return {module:client.Promise} Resolves: Group users list object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.getGroupUsers = function (groupId) {
+    const path = utils.encodeUri("/groups/$groupId/users", { $groupId: groupId });
+    return this._http.authedRequest(undefined, "GET", path);
+};
+
+/**
+ * @param {string} groupId
+ * @return {module:client.Promise} Resolves: Group users list object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.getGroupInvitedUsers = function (groupId) {
+    const path = utils.encodeUri("/groups/$groupId/invited_users", { $groupId: groupId });
+    return this._http.authedRequest(undefined, "GET", path);
+};
+
+/**
+ * @param {string} groupId
+ * @return {module:client.Promise} Resolves: Group rooms list object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.getGroupRooms = function (groupId) {
+    const path = utils.encodeUri("/groups/$groupId/rooms", { $groupId: groupId });
+    return this._http.authedRequest(undefined, "GET", path);
+};
+
+/**
+ * @param {string} groupId
+ * @param {string} userId
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.inviteUserToGroup = function (groupId, userId) {
+    const path = utils.encodeUri("/groups/$groupId/admin/users/invite/$userId", { $groupId: groupId, $userId: userId });
+    return this._http.authedRequest(undefined, "PUT", path, undefined, {});
+};
+
+/**
+ * @param {string} groupId
+ * @param {string} userId
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.removeUserFromGroup = function (groupId, userId) {
+    const path = utils.encodeUri("/groups/$groupId/admin/users/remove/$userId", { $groupId: groupId, $userId: userId });
+    return this._http.authedRequest(undefined, "PUT", path, undefined, {});
+};
+
+/**
+ * @param {string} groupId
+ * @param {string} userId
+ * @param {string} roleId Optional.
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.addUserToGroupSummary = function (groupId, userId, roleId) {
+    const path = utils.encodeUri(roleId ? "/groups/$groupId/summary/$roleId/users/$userId" : "/groups/$groupId/summary/users/$userId", { $groupId: groupId, $roleId: roleId, $userId: userId });
+    return this._http.authedRequest(undefined, "PUT", path, undefined, {});
+};
+
+/**
+ * @param {string} groupId
+ * @param {string} userId
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.removeUserFromGroupSummary = function (groupId, userId) {
+    const path = utils.encodeUri("/groups/$groupId/summary/users/$userId", { $groupId: groupId, $userId: userId });
+    return this._http.authedRequest(undefined, "DELETE", path, undefined, {});
+};
+
+/**
+ * @param {string} groupId
+ * @param {string} roomId
+ * @param {string} categoryId Optional.
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.addRoomToGroupSummary = function (groupId, roomId, categoryId) {
+    const path = utils.encodeUri(categoryId ? "/groups/$groupId/summary/$categoryId/rooms/$roomId" : "/groups/$groupId/summary/rooms/$roomId", { $groupId: groupId, $categoryId: categoryId, $roomId: roomId });
+    return this._http.authedRequest(undefined, "PUT", path, undefined, {});
+};
+
+/**
+ * @param {string} groupId
+ * @param {string} roomId
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.removeRoomFromGroupSummary = function (groupId, roomId) {
+    const path = utils.encodeUri("/groups/$groupId/summary/rooms/$roomId", { $groupId: groupId, $roomId: roomId });
+    return this._http.authedRequest(undefined, "DELETE", path, undefined, {});
+};
+
+/**
+ * @param {string} groupId
+ * @param {string} roomId
+ * @param {bool} isPublic Whether the room-group association is visible to non-members
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.addRoomToGroup = function (groupId, roomId, isPublic) {
+    if (isPublic === undefined) {
+        isPublic = true;
+    }
+    const path = utils.encodeUri("/groups/$groupId/admin/rooms/$roomId", { $groupId: groupId, $roomId: roomId });
+    return this._http.authedRequest(undefined, "PUT", path, undefined, { "m.visibility": { type: isPublic ? "public" : "private" } });
+};
+
+/**
+ * Configure the visibility of a room-group association.
+ * @param {string} groupId
+ * @param {string} roomId
+ * @param {bool} isPublic Whether the room-group association is visible to non-members
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.updateGroupRoomVisibility = function (groupId, roomId, isPublic) {
+    // NB: The /config API is generic but there's not much point in exposing this yet as synapse
+    //     is the only server to implement this. In future we should consider an API that allows
+    //     arbitrary configuration, i.e. "config/$configKey".
+
+    const path = utils.encodeUri("/groups/$groupId/admin/rooms/$roomId/config/m.visibility", { $groupId: groupId, $roomId: roomId });
+    return this._http.authedRequest(undefined, "PUT", path, undefined, { type: isPublic ? "public" : "private" });
+};
+
+/**
+ * @param {string} groupId
+ * @param {string} roomId
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.removeRoomFromGroup = function (groupId, roomId) {
+    const path = utils.encodeUri("/groups/$groupId/admin/rooms/$roomId", { $groupId: groupId, $roomId: roomId });
+    return this._http.authedRequest(undefined, "DELETE", path, undefined, {});
+};
+
+/**
+ * @param {string} groupId
+ * @param {Object} opts Additional options to send alongside the acceptance.
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.acceptGroupInvite = function (groupId, opts = null) {
+    const path = utils.encodeUri("/groups/$groupId/self/accept_invite", { $groupId: groupId });
+    return this._http.authedRequest(undefined, "PUT", path, undefined, opts || {});
+};
+
+/**
+ * @param {string} groupId
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.joinGroup = function (groupId) {
+    const path = utils.encodeUri("/groups/$groupId/self/join", { $groupId: groupId });
+    return this._http.authedRequest(undefined, "PUT", path, undefined, {});
+};
+
+/**
+ * @param {string} groupId
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.leaveGroup = function (groupId) {
+    const path = utils.encodeUri("/groups/$groupId/self/leave", { $groupId: groupId });
+    return this._http.authedRequest(undefined, "PUT", path, undefined, {});
+};
+
+/**
+ * @return {module:client.Promise} Resolves: The groups to which the user is joined
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.getJoinedGroups = function () {
+    const path = utils.encodeUri("/joined_groups");
+    return this._http.authedRequest(undefined, "GET", path);
+};
+
+/**
+ * @param {Object} content Request content
+ * @param {string} content.localpart The local part of the desired group ID
+ * @param {Object} content.profile Group profile object
+ * @return {module:client.Promise} Resolves: Object with key group_id: id of the created group
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.createGroup = function (content) {
+    const path = utils.encodeUri("/create_group");
+    return this._http.authedRequest(undefined, "POST", path, undefined, content);
+};
+
+/**
+ * @param {string[]} userIds List of user IDs
+ * @return {module:client.Promise} Resolves: Object as exmaple below
+ *
+ *     {
+ *         "users": {
+ *             "@bob:example.com": {
+ *                 "+example:example.com"
+ *             }
+ *         }
+ *     }
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.getPublicisedGroups = function (userIds) {
+    const path = utils.encodeUri("/publicised_groups");
+    return this._http.authedRequest(undefined, "POST", path, undefined, { user_ids: userIds });
+};
+
+/**
+ * @param {string} groupId
+ * @param {bool} isPublic Whether the user's membership of this group is made public
+ * @return {module:client.Promise} Resolves: Empty object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.setGroupPublicity = function (groupId, isPublic) {
+    const path = utils.encodeUri("/groups/$groupId/self/update_publicity", { $groupId: groupId });
+    return this._http.authedRequest(undefined, "PUT", path, undefined, {
+        publicise: isPublic
+    });
+};
+
+/**
  * Retrieve a state event.
  * @param {string} roomId
  * @param {string} eventType
  * @param {string} stateKey
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.getStateEvent = function(roomId, eventType, stateKey, callback) {
-    var pathParams = {
+MatrixBaseApis.prototype.getStateEvent = function (roomId, eventType, stateKey, callback) {
+    const pathParams = {
         $roomId: roomId,
         $eventType: eventType,
         $stateKey: stateKey
     };
-    var path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
+    let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
     if (stateKey !== undefined) {
         path = utils.encodeUri(path + "/$stateKey", pathParams);
     }
-    return this._http.authedRequest(
-        callback, "GET", path
-    );
+    return this._http.authedRequest(callback, "GET", path);
 };
 
 /**
  * @param {string} roomId
  * @param {string} eventType
  * @param {Object} content
  * @param {string} stateKey
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, stateKey,
-                                                 callback) {
-    var pathParams = {
+MatrixBaseApis.prototype.sendStateEvent = function (roomId, eventType, content, stateKey, callback) {
+    const pathParams = {
         $roomId: roomId,
         $eventType: eventType,
         $stateKey: stateKey
     };
-    var path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
+    let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
     if (stateKey !== undefined) {
         path = utils.encodeUri(path + "/$stateKey", pathParams);
     }
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, content
-    );
-};
-
-/**
- * @param {string} roomId
- * @param {string} eventId
- * @param {module:client.callback} callback Optional.
- * @return {module:client.Promise} Resolves: TODO
- * @return {module:http-api.MatrixError} Rejects: with an error response.
- */
-MatrixBaseApis.prototype.redactEvent = function(roomId, eventId, callback) {
-    var path = utils.encodeUri("/rooms/$roomId/redact/$eventId", {
-        $roomId: roomId,
-        $eventId: eventId
-    });
-    return this._http.authedRequest(callback, "POST", path, undefined, {});
+    return this._http.authedRequest(callback, "PUT", path, undefined, content);
 };
 
 /**
  * @param {string} roomId
  * @param {Number} limit
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
-    if (utils.isFunction(limit)) { callback = limit; limit = undefined; }
-    var path = utils.encodeUri("/rooms/$roomId/initialSync",
-        {$roomId: roomId}
-    );
+MatrixBaseApis.prototype.roomInitialSync = function (roomId, limit, callback) {
+    if (utils.isFunction(limit)) {
+        callback = limit;limit = undefined;
+    }
+    const path = utils.encodeUri("/rooms/$roomId/initialSync", { $roomId: roomId });
     if (!limit) {
         limit = 30;
     }
-    return this._http.authedRequest(
-        callback, "GET", path, { limit: limit }
-    );
+    return this._http.authedRequest(callback, "GET", path, { limit: limit });
 };
 
+/**
+ * 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 {string} roomId ID of the room that has been read
+ * @param {string} rmEventId ID of the event that has been read
+ * @param {string} 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 {object} opts Options for the read markers.
+ * @param {object} opts.hidden True to hide the read receipt from other users. <b>This
+ * property is currently unstable and may change in the future.</b>
+ * @return {module:client.Promise} Resolves: the empty object, {}.
+ */
+MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest = function (roomId, rmEventId, rrEventId, opts) {
+    const path = utils.encodeUri("/rooms/$roomId/read_markers", {
+        $roomId: roomId
+    });
+
+    const content = {
+        "m.fully_read": rmEventId,
+        "m.read": rrEventId,
+        "m.hidden": Boolean(opts ? opts.hidden : false)
+    };
+
+    return this._http.authedRequest(undefined, "POST", path, undefined, content);
+};
+
+/**
+ * @return {module:client.Promise} Resolves: A list of the user's current rooms
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.getJoinedRooms = function () {
+    const path = utils.encodeUri("/joined_rooms");
+    return this._http.authedRequest(undefined, "GET", path);
+};
+
+/**
+ * Retrieve membership info. for a room.
+ * @param {string} roomId ID of the room to get membership for
+ * @return {module:client.Promise} Resolves: A list of currently joined users
+ *                                 and their profile data.
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.getJoinedRoomMembers = function (roomId) {
+    const path = utils.encodeUri("/rooms/$roomId/joined_members", {
+        $roomId: roomId
+    });
+    return this._http.authedRequest(undefined, "GET", path);
+};
 
 // Room Directory operations
 // =========================
 
 /**
+ * @param {Object} options Options for this request
  * @param {string} options.server The remote server to query for the room list.
  *                                Optional. If unspecified, get the local home
  *                                server's public room list.
  * @param {number} options.limit Maximum number of entries to return
  * @param {string} options.since Token to paginate from
  * @param {object} options.filter Filter parameters
  * @param {string} options.filter.generic_search_term String to search for
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.publicRooms = function(options, callback) {
-    if (typeof(options) == 'function') {
+MatrixBaseApis.prototype.publicRooms = function (options, callback) {
+    if (typeof options == 'function') {
         callback = options;
         options = {};
     }
     if (options === undefined) {
         options = {};
     }
 
-    var query_params = {};
+    const query_params = {};
     if (options.server) {
         query_params.server = options.server;
         delete options.server;
     }
 
     if (Object.keys(options).length === 0 && Object.keys(query_params).length === 0) {
         return this._http.authedRequest(callback, "GET", "/publicRooms");
     } else {
-        return this._http.authedRequest(
-            callback, "POST", "/publicRooms", query_params, options
-        );
+        return this._http.authedRequest(callback, "POST", "/publicRooms", query_params, options);
     }
 };
 
 /**
  * Create an alias to room ID mapping.
  * @param {string} alias The room alias to create.
  * @param {string} roomId The room ID to link the alias to.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO.
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.createAlias = function(alias, roomId, callback) {
-    var path = utils.encodeUri("/directory/room/$alias", {
+MatrixBaseApis.prototype.createAlias = function (alias, roomId, callback) {
+    const path = utils.encodeUri("/directory/room/$alias", {
         $alias: alias
     });
-    var data = {
+    const data = {
         room_id: roomId
     };
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, data
-    );
+    return this._http.authedRequest(callback, "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 {string} alias The room alias to delete.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO.
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.deleteAlias = function(alias, callback) {
-    var path = utils.encodeUri("/directory/room/$alias", {
+MatrixBaseApis.prototype.deleteAlias = function (alias, callback) {
+    const path = utils.encodeUri("/directory/room/$alias", {
         $alias: alias
     });
-    return this._http.authedRequest(
-        callback, "DELETE", path, undefined, undefined
-    );
+    return this._http.authedRequest(callback, "DELETE", path, undefined, undefined);
 };
 
 /**
  * Get room info for the given alias.
  * @param {string} alias The room alias to resolve.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: Object with room_id and servers.
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.getRoomIdForAlias = function(alias, callback) {
+MatrixBaseApis.prototype.getRoomIdForAlias = function (alias, callback) {
     // TODO: deprecate this or resolveRoomAlias
-    var path = utils.encodeUri("/directory/room/$alias", {
+    const path = utils.encodeUri("/directory/room/$alias", {
         $alias: alias
     });
-    return this._http.authedRequest(
-        callback, "GET", path
-    );
+    return this._http.authedRequest(callback, "GET", path);
 };
 
 /**
  * @param {string} roomAlias
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.resolveRoomAlias = function(roomAlias, callback) {
+MatrixBaseApis.prototype.resolveRoomAlias = function (roomAlias, callback) {
     // TODO: deprecate this or getRoomIdForAlias
-    var path = utils.encodeUri("/directory/room/$alias", {$alias: roomAlias});
+    const path = utils.encodeUri("/directory/room/$alias", { $alias: roomAlias });
     return this._http.request(callback, "GET", path);
 };
 
 /**
  * Get the visibility of a room in the current HS's room directory
  * @param {string} roomId
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.getRoomDirectoryVisibility =
-                                function(roomId, callback) {
-    var path = utils.encodeUri("/directory/list/room/$roomId", {
+MatrixBaseApis.prototype.getRoomDirectoryVisibility = function (roomId, callback) {
+    const path = utils.encodeUri("/directory/list/room/$roomId", {
         $roomId: roomId
     });
     return this._http.authedRequest(callback, "GET", path);
 };
 
 /**
  * Set the visbility of a room in the current HS's room directory
  * @param {string} roomId
  * @param {string} visibility "public" to make the room visible
  *                 in the public directory, or "private" to make
  *                 it invisible.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: result object
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.setRoomDirectoryVisibility =
-                                function(roomId, visibility, callback) {
-    var path = utils.encodeUri("/directory/list/room/$roomId", {
+MatrixBaseApis.prototype.setRoomDirectoryVisibility = function (roomId, visibility, callback) {
+    const path = utils.encodeUri("/directory/list/room/$roomId", {
+        $roomId: roomId
+    });
+    return this._http.authedRequest(callback, "PUT", path, undefined, { "visibility": visibility });
+};
+
+/**
+ * Set the visbility of a room bridged to a 3rd party network in
+ * the current HS's room directory.
+ * @param {string} networkId the network ID of the 3rd party
+ *                 instance under which this room is published under.
+ * @param {string} roomId
+ * @param {string} visibility "public" to make the room visible
+ *                 in the public directory, or "private" to make
+ *                 it invisible.
+ * @param {module:client.callback} callback Optional.
+ * @return {module:client.Promise} Resolves: result object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService = function (networkId, roomId, visibility, callback) {
+    const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", {
+        $networkId: networkId,
         $roomId: roomId
     });
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, { "visibility": visibility }
-    );
+    return this._http.authedRequest(callback, "PUT", path, undefined, { "visibility": visibility });
 };
 
+// User Directory Operations
+// =========================
+
+/**
+ * Query the user directory with a term matching user IDs, display names and domains.
+ * @param {object} opts options
+ * @param {string} opts.term the term with which to search.
+ * @param {number} opts.limit the maximum number of results to return. The server will
+ *                 apply a limit if unspecified.
+ * @return {module:client.Promise} Resolves: an array of results.
+ */
+MatrixBaseApis.prototype.searchUserDirectory = function (opts) {
+    const body = {
+        search_term: opts.term
+    };
+
+    if (opts.limit !== undefined) {
+        body.limit = opts.limit;
+    }
+
+    return this._http.authedRequest(undefined, "POST", "/user_directory/search", undefined, body);
+};
 
 // Media operations
 // ================
 
 /**
  * Upload a file to the media repository on the home server.
  *
  * @param {object} 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 {object} opts  options object
  *
  * @param {string=} opts.name   Name to give the file on the server. Defaults
  *   to <tt>file.name</tt>.
  *
+ * @param {boolean=} opts.includeFilename if false will not send the filename,
+ *   e.g for encrypted file uploads where filename leaks are undesirable.
+ *   Defaults to true.
+ *
  * @param {string=} opts.type   Content-type for the upload. Defaults to
  *   <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
  *
  * @param {boolean=} opts.rawResponse Return the raw body, rather than
  *   parsing the JSON. Defaults to false (except on node.js, where it
  *   defaults to true for backwards compatibility).
  *
  * @param {boolean=} opts.onlyContentUri Just return the content URI,
  *   rather than the whole body. Defaults to false (except on browsers,
  *   where it defaults to true for backwards compatibility). Ignored if
  *   opts.rawResponse is true.
  *
  * @param {Function=} opts.callback Deprecated. Optional. The callback to
  *    invoke on success/failure. See the promise return values for more
  *    information.
  *
+ * @param {Function=} opts.progressHandler Optional. Called when a chunk of
+ *    data has been uploaded, with an object containing the fields `loaded`
+ *    (number of bytes transferred) and `total` (total size, if known).
+ *
  * @return {module:client.Promise} Resolves to response object, as
  *    determined by this.opts.onlyData, opts.rawResponse, and
  *    opts.onlyContentUri.  Rejects with an error (usually a MatrixError).
  */
-MatrixBaseApis.prototype.uploadContent = function(file, opts) {
+MatrixBaseApis.prototype.uploadContent = function (file, opts) {
     return this._http.uploadContent(file, opts);
 };
 
 /**
  * Cancel a file upload in progress
  * @param {module:client.Promise} promise The promise returned from uploadContent
  * @return {boolean} true if canceled, otherwise false
  */
-MatrixBaseApis.prototype.cancelUpload = function(promise) {
+MatrixBaseApis.prototype.cancelUpload = function (promise) {
     return this._http.cancelUpload(promise);
 };
 
 /**
  * Get a list of all file uploads in progress
  * @return {array} 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
  */
-MatrixBaseApis.prototype.getCurrentUploads = function() {
+MatrixBaseApis.prototype.getCurrentUploads = function () {
     return this._http.getCurrentUploads();
 };
 
-
 // Profile operations
 // ==================
 
 /**
  * @param {string} userId
  * @param {string} info The kind of info to retrieve (e.g. 'displayname',
  * 'avatar_url').
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.getProfileInfo = function(userId, info, callback) {
-    if (utils.isFunction(info)) { callback = info; info = undefined; }
+MatrixBaseApis.prototype.getProfileInfo = function (userId, info, callback) {
+    if (utils.isFunction(info)) {
+        callback = info;info = undefined;
+    }
 
-    var path = info ?
-    utils.encodeUri("/profile/$userId/$info",
-             { $userId: userId, $info: info }) :
-    utils.encodeUri("/profile/$userId",
-             { $userId: userId });
+    const path = info ? utils.encodeUri("/profile/$userId/$info", { $userId: userId, $info: info }) : utils.encodeUri("/profile/$userId", { $userId: userId });
     return this._http.authedRequest(callback, "GET", path);
 };
 
-
 // Account operations
 // ==================
 
 /**
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.getThreePids = function(callback) {
-    var path = "/account/3pid";
-    return this._http.authedRequest(
-        callback, "GET", path, undefined, undefined
-    );
+MatrixBaseApis.prototype.getThreePids = function (callback) {
+    const path = "/account/3pid";
+    return this._http.authedRequest(callback, "GET", path, undefined, undefined);
 };
 
 /**
+ * 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.
+ *
  * @param {Object} creds
  * @param {boolean} bind
  * @param {module:client.callback} callback Optional.
- * @return {module:client.Promise} Resolves: TODO
+ * @return {module:client.Promise} Resolves: on success
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
-    var path = "/account/3pid";
-    var data = {
+MatrixBaseApis.prototype.addThreePid = function (creds, bind, callback) {
+    const path = "/account/3pid";
+    const data = {
         'threePidCreds': creds,
         'bind': bind
     };
-    return this._http.authedRequest(
-        callback, "POST", path, null, data
-    );
+    return this._http.authedRequest(callback, "POST", path, null, 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 {Object} data A object with 3PID validation data from having called
+ * `account/3pid/<medium>/requestToken` on the homeserver.
+ * @return {module:client.Promise} Resolves: on success
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.addThreePidOnly = function (data) {
+    const path = "/account/3pid/add";
+    return this._http.authedRequest(undefined, "POST", path, null, data, {
+        prefix: httpApi.PREFIX_UNSTABLE
+    });
+};
+
+/**
+ * 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 {Object} 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.
+ * @return {module:client.Promise} Resolves: on success
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.bindThreePid = function (data) {
+    const path = "/account/3pid/bind";
+    return this._http.authedRequest(undefined, "POST", path, null, data, {
+        prefix: httpApi.PREFIX_UNSTABLE
+    });
+};
+
+/**
+ * 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 {string} medium The threepid medium (eg. 'email')
+ * @param {string} address The threepid address (eg. 'bob@example.com')
+ *        this must be as returned by getThreePids.
+ * @return {module:client.Promise} Resolves: on success
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.unbindThreePid = function (medium, address) {
+    const path = "/account/3pid/unbind";
+    const data = {
+        medium,
+        address,
+        id_server: this.getIdentityServerUrl(true)
+    };
+    return this._http.authedRequest(undefined, "POST", path, null, data, {
+        prefix: httpApi.PREFIX_UNSTABLE
+    });
+};
+
+/**
+ * @param {string} medium The threepid medium (eg. 'email')
+ * @param {string} address The threepid address (eg. 'bob@example.com')
+ *        this must be as returned by getThreePids.
+ * @return {module:client.Promise} Resolves: The server response on success
+ *     (generally the empty JSON object)
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.deleteThreePid = function (medium, address) {
+    const path = "/account/3pid/delete";
+    const data = {
+        'medium': medium,
+        'address': address
+    };
+    return this._http.authedRequest(undefined, "POST", path, null, data);
 };
 
 /**
  * Make a request to change your password.
  * @param {Object} authDict
  * @param {string} newPassword The new desired password.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback) {
-    var path = "/account/password";
-    var data = {
+MatrixBaseApis.prototype.setPassword = function (authDict, newPassword, callback) {
+    const path = "/account/password";
+    const data = {
         'auth': authDict,
         'new_password': newPassword
     };
 
-    return this._http.authedRequest(
-        callback, "POST", path, null, data
-    );
+    return this._http.authedRequest(callback, "POST", path, null, data);
 };
 
-
 // Device operations
 // =================
 
 /**
  * Gets all devices recorded for the logged-in user
  * @return {module:client.Promise} Resolves: result object
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.getDevices = function() {
-    var path = "/devices";
-    return this._http.authedRequestWithPrefix(
-        undefined, "GET", path, undefined, undefined,
-        httpApi.PREFIX_UNSTABLE
-    );
+MatrixBaseApis.prototype.getDevices = function () {
+    return this._http.authedRequest(undefined, 'GET', "/devices", undefined, undefined);
 };
 
 /**
  * Update the given device
  *
  * @param {string} device_id  device to update
  * @param {Object} body       body of request
  * @return {module:client.Promise} Resolves: result object
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
-    var path = utils.encodeUri("/devices/$device_id", {
-        $device_id: device_id,
+MatrixBaseApis.prototype.setDeviceDetails = function (device_id, body) {
+    const path = utils.encodeUri("/devices/$device_id", {
+        $device_id: device_id
     });
 
-
-    return this._http.authedRequestWithPrefix(
-        undefined, "PUT", path, undefined, body,
-        httpApi.PREFIX_UNSTABLE
-    );
+    return this._http.authedRequest(undefined, "PUT", path, undefined, body);
 };
 
 /**
  * Delete the given device
  *
  * @param {string} device_id  device to delete
  * @param {object} auth Optional. Auth data to supply for User-Interactive auth.
  * @return {module:client.Promise} Resolves: result object
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
-    var path = utils.encodeUri("/devices/$device_id", {
-        $device_id: device_id,
+MatrixBaseApis.prototype.deleteDevice = function (device_id, auth) {
+    const path = utils.encodeUri("/devices/$device_id", {
+        $device_id: device_id
     });
 
-    var body = {};
+    const body = {};
 
     if (auth) {
         body.auth = auth;
     }
 
-    return this._http.authedRequestWithPrefix(
-        undefined, "DELETE", path, undefined, body,
-        httpApi.PREFIX_UNSTABLE
-    );
+    return this._http.authedRequest(undefined, "DELETE", path, undefined, body);
 };
 
+/**
+ * Delete multiple device
+ *
+ * @param {string[]} devices IDs of the devices to delete
+ * @param {object} auth Optional. Auth data to supply for User-Interactive auth.
+ * @return {module:client.Promise} Resolves: result object
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.deleteMultipleDevices = function (devices, auth) {
+    const body = { devices };
+
+    if (auth) {
+        body.auth = auth;
+    }
+
+    const path = "/delete_devices";
+    return this._http.authedRequest(undefined, "POST", path, undefined, body);
+};
 
 // Push operations
 // ===============
 
 /**
  * Gets all pushers registered for the logged-in user
  *
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: Array of objects representing pushers
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.getPushers = function(callback) {
-    var path = "/pushers";
-    return this._http.authedRequest(
-        callback, "GET", path, undefined, undefined
-    );
+MatrixBaseApis.prototype.getPushers = function (callback) {
+    const path = "/pushers";
+    return this._http.authedRequest(callback, "GET", path, undefined, undefined);
 };
 
 /**
  * Adds a new pusher or updates an existing pusher
  *
  * @param {Object} pusher Object representing a pusher
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: Empty json object on success
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.setPusher = function(pusher, callback) {
-    var path = "/pushers/set";
-    return this._http.authedRequest(
-        callback, "POST", path, null, pusher
-    );
+MatrixBaseApis.prototype.setPusher = function (pusher, callback) {
+    const path = "/pushers/set";
+    return this._http.authedRequest(callback, "POST", path, null, pusher);
 };
 
 /**
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.getPushRules = function(callback) {
-    return this._http.authedRequest(callback, "GET", "/pushrules/");
+MatrixBaseApis.prototype.getPushRules = function (callback) {
+    return this._http.authedRequest(callback, "GET", "/pushrules/").then(rules => {
+        return PushProcessor.rewriteDefaultRules(rules);
+    });
 };
 
 /**
  * @param {string} scope
  * @param {string} kind
  * @param {string} ruleId
  * @param {Object} body
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.addPushRule = function(scope, kind, ruleId, body, callback) {
+MatrixBaseApis.prototype.addPushRule = function (scope, kind, ruleId, body, callback) {
     // NB. Scope not uri encoded because devices need the '/'
-    var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
+    const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
         $kind: kind,
         $ruleId: ruleId
     });
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, body
-    );
+    return this._http.authedRequest(callback, "PUT", path, undefined, body);
 };
 
 /**
  * @param {string} scope
  * @param {string} kind
  * @param {string} ruleId
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.deletePushRule = function(scope, kind, ruleId, callback) {
+MatrixBaseApis.prototype.deletePushRule = function (scope, kind, ruleId, callback) {
     // NB. Scope not uri encoded because devices need the '/'
-    var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
+    const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
         $kind: kind,
         $ruleId: ruleId
     });
     return this._http.authedRequest(callback, "DELETE", path);
 };
 
 /**
  * Enable or disable a push notification rule.
  * @param {string} scope
  * @param {string} kind
  * @param {string} ruleId
  * @param {boolean} enabled
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: result object
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.setPushRuleEnabled = function(scope, kind,
-                                                     ruleId, enabled, callback) {
-    var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", {
+MatrixBaseApis.prototype.setPushRuleEnabled = function (scope, kind, ruleId, enabled, callback) {
+    const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", {
         $kind: kind,
         $ruleId: ruleId
     });
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, {"enabled": enabled}
-    );
+    return this._http.authedRequest(callback, "PUT", path, undefined, { "enabled": enabled });
 };
 
 /**
  * Set the actions for a push notification rule.
  * @param {string} scope
  * @param {string} kind
  * @param {string} ruleId
  * @param {array} actions
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: result object
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.setPushRuleActions = function(scope, kind,
-                                                     ruleId, actions, callback) {
-    var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", {
+MatrixBaseApis.prototype.setPushRuleActions = function (scope, kind, ruleId, actions, callback) {
+    const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", {
         $kind: kind,
         $ruleId: ruleId
     });
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, {"actions": actions}
-    );
+    return this._http.authedRequest(callback, "PUT", path, undefined, { "actions": actions });
 };
 
-
 // Search
 // ======
 
 /**
  * Perform a server-side search.
  * @param {Object} opts
  * @param {string} opts.next_batch the batch token to pass in the query string
  * @param {Object} opts.body the JSON object to pass to the request body.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.search = function(opts, callback) {
-    var queryparams = {};
+MatrixBaseApis.prototype.search = function (opts, callback) {
+    const queryparams = {};
     if (opts.next_batch) {
         queryparams.next_batch = opts.next_batch;
     }
-    return this._http.authedRequest(
-        callback, "POST", "/search", queryparams, opts.body
-    );
+    return this._http.authedRequest(callback, "POST", "/search", queryparams, opts.body);
 };
 
 // Crypto
 // ======
 
 /**
  * Upload keys
  *
@@ -927,208 +1547,620 @@ MatrixBaseApis.prototype.search = functi
  * @param {string=} opts.device_id  explicit device_id to use for upload
  *    (default is to use the same as that used during auth).
  *
  * @param {module:client.callback=} callback
  *
  * @return {module:client.Promise} Resolves: result object. Rejects: with
  *     an error response ({@link module:http-api.MatrixError}).
  */
-MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
+MatrixBaseApis.prototype.uploadKeysRequest = function (content, opts, callback) {
     opts = opts || {};
-    var deviceId = opts.device_id;
-    var path;
+    const deviceId = opts.device_id;
+    let path;
     if (deviceId) {
         path = utils.encodeUri("/keys/upload/$deviceId", {
-            $deviceId: deviceId,
+            $deviceId: deviceId
         });
     } else {
         path = "/keys/upload";
     }
-    return this._http.authedRequestWithPrefix(
-        callback, "POST", path, undefined, content, httpApi.PREFIX_UNSTABLE
-    );
+    return this._http.authedRequest(callback, "POST", path, undefined, content);
 };
 
 /**
  * Download device keys
  *
  * @param {string[]} userIds  list of users to get keys for
  *
- * @param {module:client.callback=} callback
+ * @param {Object=} opts
+ *
+ * @param {string=} opts.token   sync token to pass in the query request, to help
+ *   the HS give the most recent results
  *
  * @return {module:client.Promise} Resolves: result object. Rejects: with
  *     an error response ({@link module:http-api.MatrixError}).
  */
-MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, callback) {
-    var downloadQuery = {};
-
-    for (var i = 0; i < userIds.length; ++i) {
-        downloadQuery[userIds[i]] = {};
+MatrixBaseApis.prototype.downloadKeysForUsers = function (userIds, opts) {
+    if (utils.isFunction(opts)) {
+        // opts used to be 'callback'.
+        throw new Error('downloadKeysForUsers no longer accepts a callback parameter');
     }
-    var content = {device_keys: downloadQuery};
-    return this._http.authedRequestWithPrefix(
-        callback, "POST", "/keys/query", undefined, content,
-        httpApi.PREFIX_UNSTABLE
-    );
+    opts = opts || {};
+
+    const content = {
+        device_keys: {}
+    };
+    if ('token' in opts) {
+        content.token = opts.token;
+    }
+    userIds.forEach(u => {
+        content.device_keys[u] = {};
+    });
+
+    return this._http.authedRequest(undefined, "POST", "/keys/query", undefined, content);
 };
 
 /**
  * Claim one-time keys
  *
- * @param {string[][]} devices  a list of [userId, deviceId] pairs
+ * @param {string[]} devices  a list of [userId, deviceId] pairs
  *
  * @param {string} [key_algorithm = signed_curve25519]  desired key type
  *
  * @return {module:client.Promise} Resolves: result object. Rejects: with
  *     an error response ({@link module:http-api.MatrixError}).
  */
-MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
-    var queries = {};
+MatrixBaseApis.prototype.claimOneTimeKeys = function (devices, key_algorithm) {
+    const queries = {};
 
     if (key_algorithm === undefined) {
         key_algorithm = "signed_curve25519";
     }
 
-    for (var i = 0; i < devices.length; ++i) {
-        var userId = devices[i][0];
-        var deviceId = devices[i][1];
-        var query = queries[userId] || {};
+    for (let i = 0; i < devices.length; ++i) {
+        const userId = devices[i][0];
+        const deviceId = devices[i][1];
+        const query = queries[userId] || {};
         queries[userId] = query;
         query[deviceId] = key_algorithm;
     }
-    var content = {one_time_keys: queries};
-    return this._http.authedRequestWithPrefix(
-        undefined, "POST", "/keys/claim", undefined, content,
-        httpApi.PREFIX_UNSTABLE
-    );
+    const content = { one_time_keys: queries };
+    const path = "/keys/claim";
+    return this._http.authedRequest(undefined, "POST", path, undefined, content);
 };
 
+/**
+ * Ask the server for a list of users who have changed their device lists
+ * between a pair of sync tokens
+ *
+ * @param {string} oldToken
+ * @param {string} newToken
+ *
+ * @return {module:client.Promise} Resolves: result object. Rejects: with
+ *     an error response ({@link module:http-api.MatrixError}).
+ */
+MatrixBaseApis.prototype.getKeyChanges = function (oldToken, newToken) {
+    const qps = {
+        from: oldToken,
+        to: newToken
+    };
+
+    const path = "/keys/changes";
+    return this._http.authedRequest(undefined, "GET", path, qps, undefined);
+};
 
 // Identity Server Operations
 // ==========================
 
 /**
- * Requests an email verification token directly from an Identity Server.
+ * Register with an Identity Server using the OpenID token from the user's
+ * Homeserver, which can be retrieved via
+ * {@link module:client~MatrixClient#getOpenIdToken}.
+ *
+ * Note that the `/account/register` endpoint (as well as IS authentication in
+ * general) was added as part of the v2 API version.
  *
- * Note that the Home Server offers APIs to proxy this API for specific
- * situations, allowing for better feedback to the user.
+ * @param {object} hsOpenIdToken
+ * @return {module:client.Promise} Resolves: with object containing an Identity
+ * Server access token.
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.registerWithIdentityServer = function (hsOpenIdToken) {
+    if (!this.idBaseUrl) {
+        throw new Error("No Identity Server base URL set");
+    }
+
+    const uri = this.idBaseUrl + httpApi.PREFIX_IDENTITY_V2 + "/account/register";
+    return this._http.requestOtherUrl(undefined, "POST", uri, null, 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 {string} email The email address to request a token for
  * @param {string} clientSecret A secret binary string generated by the client.
  *                 It is recommended this be around 16 ASCII characters.
  * @param {number} 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 {string} nextLink Optional If specified, the client will be redirected
  *                 to this link after validation.
  * @param {module:client.callback} callback Optional.
+ * @param {string} identityAccessToken The `access_token` field of the identity
+ * server `/account/register` response (see {@link registerWithIdentityServer}).
+ *
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
- * @throws Error if No ID server is set
+ * @throws Error if no identity server is set
  */
-MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret,
-                                                    sendAttempt, nextLink, callback) {
-    var params = {
+MatrixBaseApis.prototype.requestEmailToken = async function (email, clientSecret, sendAttempt, nextLink, callback, identityAccessToken) {
+    const params = {
         client_secret: clientSecret,
         email: email,
         send_attempt: sendAttempt,
         next_link: nextLink
     };
-    return this._http.idServerRequest(
-        callback, "POST", "/validate/email/requestToken",
-        params, httpApi.PREFIX_IDENTITY_V1
-    );
+
+    try {
+        const response = await this._http.idServerRequest(undefined, "POST", "/validate/email/requestToken", params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken);
+        // TODO: Fold callback into above call once v1 path below is removed
+        if (callback) callback(null, response);
+        return response;
+    } catch (err) {
+        if (err.cors === "rejected" || err.httpStatus === 404) {
+            // Fall back to deprecated v1 API for now
+            // TODO: Remove this path once v2 is only supported version
+            // See https://github.com/vector-im/riot-web/issues/10443
+            _logger2.default.warn("IS doesn't support v2, falling back to deprecated v1");
+            return await this._http.idServerRequest(callback, "POST", "/validate/email/requestToken", params, httpApi.PREFIX_IDENTITY_V1);
+        }
+        if (callback) callback(err);
+        throw err;
+    }
+};
+
+/**
+ * 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 {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in
+ *                 which phoneNumber should be parsed relative to.
+ * @param {string} phoneNumber The phone number, in national or international
+ *                 format
+ * @param {string} clientSecret A secret binary string generated by the client.
+ *                 It is recommended this be around 16 ASCII characters.
+ * @param {number} 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 {string} nextLink Optional If specified, the client will be redirected
+ *                 to this link after validation.
+ * @param {module:client.callback} callback Optional.
+ * @param {string} identityAccessToken The `access_token` field of the Identity
+ * Server `/account/register` response (see {@link registerWithIdentityServer}).
+ *
+ * @return {module:client.Promise} Resolves: TODO
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ * @throws Error if no identity server is set
+ */
+MatrixBaseApis.prototype.requestMsisdnToken = async function (phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink, callback, identityAccessToken) {
+    const params = {
+        client_secret: clientSecret,
+        country: phoneCountry,
+        phone_number: phoneNumber,
+        send_attempt: sendAttempt,
+        next_link: nextLink
+    };
+
+    try {
+        const response = await this._http.idServerRequest(undefined, "POST", "/validate/msisdn/requestToken", params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken);
+        // TODO: Fold callback into above call once v1 path below is removed
+        if (callback) callback(null, response);
+        return response;
+    } catch (err) {
+        if (err.cors === "rejected" || err.httpStatus === 404) {
+            // Fall back to deprecated v1 API for now
+            // TODO: Remove this path once v2 is only supported version
+            // See https://github.com/vector-im/riot-web/issues/10443
+            _logger2.default.warn("IS doesn't support v2, falling back to deprecated v1");
+            return await this._http.idServerRequest(callback, "POST", "/validate/msisdn/requestToken", params, httpApi.PREFIX_IDENTITY_V1);
+        }
+        if (callback) callback(err);
+        throw err;
+    }
+};
+
+/**
+ * Submits a MSISDN token to the identity server
+ *
+ * This is used when submitting the code sent by SMS to a phone number.
+ * The ID 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 {string} sid The sid given in the response to requestToken
+ * @param {string} clientSecret A secret binary string generated by the client.
+ *                 This must be the same value submitted in the requestToken call.
+ * @param {string} msisdnToken The MSISDN token, as enetered by the user.
+ * @param {string} identityAccessToken The `access_token` field of the Identity
+ * Server `/account/register` response (see {@link registerWithIdentityServer}).
+ *
+ * @return {module:client.Promise} Resolves: Object, currently with no parameters.
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ * @throws Error if No ID server is set
+ */
+MatrixBaseApis.prototype.submitMsisdnToken = async function (sid, clientSecret, msisdnToken, identityAccessToken) {
+    const params = {
+        sid: sid,
+        client_secret: clientSecret,
+        token: msisdnToken
+    };
+
+    try {
+        return await this._http.idServerRequest(undefined, "POST", "/validate/msisdn/submitToken", params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken);
+    } catch (err) {
+        if (err.cors === "rejected" || err.httpStatus === 404) {
+            // Fall back to deprecated v1 API for now
+            // TODO: Remove this path once v2 is only supported version
+            // See https://github.com/vector-im/riot-web/issues/10443
+            _logger2.default.warn("IS doesn't support v2, falling back to deprecated v1");
+            return await this._http.idServerRequest(undefined, "POST", "/validate/msisdn/submitToken", params, httpApi.PREFIX_IDENTITY_V1);
+        }
+        throw err;
+    }
+};
+
+/**
+ * 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 {string} url The URL to submit the token to
+ * @param {string} sid The sid given in the response to requestToken
+ * @param {string} clientSecret A secret binary string generated by the client.
+ *                 This must be the same value submitted in the requestToken call.
+ * @param {string} msisdnToken The MSISDN token, as enetered by the user.
+ *
+ * @return {module:client.Promise} Resolves: Object, currently with no parameters.
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.submitMsisdnTokenOtherUrl = function (url, sid, clientSecret, msisdnToken) {
+    const params = {
+        sid: sid,
+        client_secret: clientSecret,
+        token: msisdnToken
+    };
+
+    return this._http.requestOtherUrl(undefined, "POST", url, undefined, params);
+};
+
+/**
+ * Gets the V2 hashing information from the identity server. Primarily useful for
+ * lookups.
+ * @param {string} identityAccessToken The access token for the identity server.
+ * @returns {Promise<object>} The hashing information for the identity server.
+ */
+MatrixBaseApis.prototype.getIdentityHashDetails = function (identityAccessToken) {
+    return this._http.idServerRequest(undefined, "GET", "/hash_details", null, httpApi.PREFIX_IDENTITY_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 {Array<Array<string,string>>} 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 {string} identityAccessToken The access token for the identity server.
+ * @returns {Promise<Array<{address, mxid}>>} A collection of address mappings to
+ * found MXIDs. Results where no user could be found will not be listed.
+ */
+MatrixBaseApis.prototype.identityHashedLookup = async function (addressPairs, // [["email@example.org", "email"], ["10005550000", "msisdn"]]
+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(undefined, "POST", "/lookup", params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken);
+
+    if (!response || !response['mappings']) return []; // no results
+
+    const foundAddresses = [/* {address: "plain@example.org", mxid} */];
+    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 {string} medium The medium of the threepid, eg. 'email'
  * @param {string} address The textual address of the threepid
  * @param {module:client.callback} callback Optional.
+ * @param {string} identityAccessToken The `access_token` field of the Identity
+ * Server `/account/register` response (see {@link registerWithIdentityServer}).
+ *
  * @return {module:client.Promise} Resolves: A threepid mapping
  *                                 object or the empty object if no mapping
  *                                 exists
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixBaseApis.prototype.lookupThreePid = function(medium, address, callback) {
-    var params = {
-        medium: medium,
-        address: address,
-    };
-    return this._http.idServerRequest(
-        callback, "GET", "/lookup",
-        params, httpApi.PREFIX_IDENTITY_V1
-    );
+MatrixBaseApis.prototype.lookupThreePid = async function (medium, address, callback, identityAccessToken) {
+    try {
+        // 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) {
+            // TODO: Fold callback into above call once v1 path below is removed
+            if (callback) callback(null, {});
+            return {};
+        }
+
+        const mapping = {
+            address,
+            medium,
+            mxid: result.mxid
+
+            // We can't reasonably fill these parameters:
+            // not_before
+            // not_after
+            // ts
+            // signatures
+        };
+
+        // TODO: Fold callback into above call once v1 path below is removed
+        if (callback) callback(null, mapping);
+        return mapping;
+    } catch (err) {
+        if (err.cors === "rejected" || err.httpStatus === 404) {
+            // Fall back to deprecated v1 API for now
+            // TODO: Remove this path once v2 is only supported version
+            // See https://github.com/vector-im/riot-web/issues/10443
+            const params = {
+                medium: medium,
+                address: address
+            };
+            _logger2.default.warn("IS doesn't support v2, falling back to deprecated v1");
+            return await this._http.idServerRequest(callback, "GET", "/lookup", params, httpApi.PREFIX_IDENTITY_V1);
+        }
+        if (callback) callback(err, undefined);
+        throw err;
+    }
 };
 
+/**
+ * Looks up the public Matrix ID mappings for multiple 3PIDs.
+ *
+ * @param {Array.<Array.<string>>} query Array of arrays containing
+ * [medium, address]
+ * @param {string} identityAccessToken The `access_token` field of the Identity
+ * Server `/account/register` response (see {@link registerWithIdentityServer}).
+ *
+ * @return {module:client.Promise} Resolves: Lookup results from IS.
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.bulkLookupThreePids = async function (query, identityAccessToken) {
+    try {
+        // 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 };
+    } catch (err) {
+        if (err.cors === "rejected" || err.httpStatus === 404) {
+            // Fall back to deprecated v1 API for now
+            // TODO: Remove this path once v2 is only supported version
+            // See https://github.com/vector-im/riot-web/issues/10443
+            const params = {
+                threepids: query
+            };
+            _logger2.default.warn("IS doesn't support v2, falling back to deprecated v1");
+            return await this._http.idServerRequest(undefined, "POST", "/bulk_lookup", params, httpApi.PREFIX_IDENTITY_V1, identityAccessToken);
+        }
+        throw err;
+    }
+};
+
+/**
+ * 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 {string} identityAccessToken The `access_token` field of the Identity
+ * Server `/account/register` response (see {@link registerWithIdentityServer}).
+ *
+ * @return {module:client.Promise} Resolves: an object with account info.
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixBaseApis.prototype.getIdentityAccount = function (identityAccessToken) {
+    return this._http.idServerRequest(undefined, "GET", "/account", undefined, httpApi.PREFIX_IDENTITY_V2, identityAccessToken);
+};
 
 // Direct-to-device messaging
 // ==========================
 
 /**
  * Send an event to a specific list of devices
  *
  * @param {string} eventType  type of event to send
  * @param {Object.<string, Object<string, Object>>} contentMap
  *    content to send. Map from user_id to device_id to content object.
  * @param {string=} txnId     transaction id. One will be made up if not
  *    supplied.
  * @return {module:client.Promise} Resolves to the result object
  */
-MatrixBaseApis.prototype.sendToDevice = function(
-    eventType, contentMap, txnId
-) {
-    var path = utils.encodeUri("/sendToDevice/$eventType/$txnId", {
+MatrixBaseApis.prototype.sendToDevice = function (eventType, contentMap, txnId) {
+    const path = utils.encodeUri("/sendToDevice/$eventType/$txnId", {
         $eventType: eventType,
-        $txnId: txnId ? txnId : this.makeTxnId(),
+        $txnId: txnId ? txnId : this.makeTxnId()
     });
 
-    var body = {
-        messages: contentMap,
+    const body = {
+        messages: contentMap
     };
 
-    return this._http.authedRequestWithPrefix(
-        undefined, "PUT", path, undefined, body,
-        httpApi.PREFIX_UNSTABLE
-    );
+    return this._http.authedRequest(undefined, "PUT", path, undefined, body);
 };
 
 // Third party Lookup API
 // ======================
 
 /**
  * Get the third party protocols that can be reached using
  * this HS
  * @return {module:client.Promise} Resolves to the result object
  */
-MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
-    return this._http.authedRequestWithPrefix(
-        undefined, "GET", "/thirdparty/protocols", undefined, undefined,
-        httpApi.PREFIX_UNSTABLE
-    );
+MatrixBaseApis.prototype.getThirdpartyProtocols = function () {
+    return this._http.authedRequest(undefined, "GET", "/thirdparty/protocols", undefined, undefined).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 {string} protocol The protocol given in getThirdpartyProtocols()
- * @param {object} params Protocol-specific parameters, as given in th
+ * @param {object} params Protocol-specific parameters, as given in the
+ *                        response to getThirdpartyProtocols()
+ * @return {module:client.Promise} Resolves to the result object
+ */
+MatrixBaseApis.prototype.getThirdpartyLocation = function (protocol, params) {
+    const path = utils.encodeUri("/thirdparty/location/$protocol", {
+        $protocol: protocol
+    });
+
+    return this._http.authedRequest(undefined, "GET", path, params, undefined);
+};
+
+/**
+ * Get information on how a specific user on a third party protocol
+ * may be reached.
+ * @param {string} protocol The protocol given in getThirdpartyProtocols()
+ * @param {object} params Protocol-specific parameters, as given in the
  *                        response to getThirdpartyProtocols()
  * @return {module:client.Promise} Resolves to the result object
  */
-MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
-    var path = utils.encodeUri("/thirdparty/location/$protocol", {
+MatrixBaseApis.prototype.getThirdpartyUser = function (protocol, params) {
+    const path = utils.encodeUri("/thirdparty/user/$protocol", {
         $protocol: protocol
     });
 
-    return this._http.authedRequestWithPrefix(
-        undefined, "GET", path, params, undefined,
-        httpApi.PREFIX_UNSTABLE
-    );
+    return this._http.authedRequest(undefined, "GET", path, params, undefined);
+};
+
+MatrixBaseApis.prototype.getTerms = function (serviceType, baseUrl) {
+    const url = termsUrlForService(serviceType, baseUrl);
+    return this._http.requestOtherUrl(undefined, 'GET', url);
+};
+
+MatrixBaseApis.prototype.agreeToTerms = function (serviceType, baseUrl, accessToken, termsUrls) {
+    const url = termsUrlForService(serviceType, baseUrl);
+    const headers = {
+        Authorization: "Bearer " + accessToken
+    };
+    return this._http.requestOtherUrl(undefined, 'POST', url, null, { user_accepts: termsUrls }, { headers });
+};
+
+/**
+ * Reports an event as inappropriate to the server, which may then notify the appropriate people.
+ * @param {string} roomId The room in which the event being reported is located.
+ * @param {string} eventId The event to report.
+ * @param {number} score The score to rate this content as where -100 is most offensive and 0 is inoffensive.
+ * @param {string} reason The reason the content is being reported. May be blank.
+ * @returns {module:client.Promise} Resolves to an empty object if successful
+ */
+MatrixBaseApis.prototype.reportEvent = function (roomId, eventId, score, reason) {
+    const path = utils.encodeUri("/rooms/$roomId/report/$eventId", {
+        $roomId: roomId,
+        $eventId: eventId
+    });
+
+    return this._http.authedRequest(undefined, "POST", path, null, { score, reason });
 };
 
 /**
  * MatrixBaseApis object
  */
-module.exports = MatrixBaseApis;
+module.exports = MatrixBaseApis;
\ No newline at end of file
--- a/chat/protocols/matrix/lib/matrix-sdk/client.js
+++ b/chat/protocols/matrix/lib/matrix-sdk/client.js
@@ -1,58 +1,107 @@
 /*
 Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2017 Vector Creations Ltd
+Copyright 2018-2019 New Vector Ltd
+Copyright 2019 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
     http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */
 "use strict";
 
-var PushProcessor = require('./pushprocessor');
+var _bluebird = require("bluebird");
+
+var _bluebird2 = _interopRequireDefault(_bluebird);
+
+var _ReEmitter = require("./ReEmitter");
+
+var _ReEmitter2 = _interopRequireDefault(_ReEmitter);
+
+var _RoomList = require("./crypto/RoomList");
+
+var _RoomList2 = _interopRequireDefault(_RoomList);
+
+var _logger = require("../src/logger");
+
+var _logger2 = _interopRequireDefault(_logger);
+
+var _crypto = require("./crypto");
+
+var _crypto2 = _interopRequireDefault(_crypto);
+
+var _recoverykey = require("./crypto/recoverykey");
+
+var _backup_password = require("./crypto/backup_password");
+
+var _randomstring = require("./randomstring");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const PushProcessor = require('./pushprocessor');
 
 /**
  * This is an internal module. See {@link MatrixClient} for the public class.
  * @module client
  */
-var EventEmitter = require("events").EventEmitter;
-var q = require("q");
-var url = require('url');
-
-var httpApi = require("./http-api");
-var MatrixEvent = require("./models/event").MatrixEvent;
-var EventStatus = require("./models/event").EventStatus;
-var EventTimeline = require("./models/event-timeline");
-var SearchResult = require("./models/search-result");
-var StubStore = require("./store/stub");
-var webRtcCall = require("./webrtc/call");
-var utils = require("./utils");
-var contentRepo = require("./content-repo");
-var Filter = require("./filter");
-var SyncApi = require("./sync");
-var MatrixBaseApis = require("./base-apis");
-var MatrixError = httpApi.MatrixError;
-
-var SCROLLBACK_DELAY_MS = 3000;
-var CRYPTO_ENABLED = false;
-
-try {
-    var Crypto = require("./crypto");
-    CRYPTO_ENABLED = true;
-} catch (e) {
-    console.error("olm load error", e);
-    // Olm not installed.
+const EventEmitter = require("events").EventEmitter;
+
+const url = require('url');
+
+const httpApi = require("./http-api");
+const MatrixEvent = require("./models/event").MatrixEvent;
+const EventStatus = require("./models/event").EventStatus;
+const EventTimeline = require("./models/event-timeline");
+const SearchResult = require("./models/search-result");
+const StubStore = require("./store/stub");
+const webRtcCall = require("./webrtc/call");
+const utils = require("./utils");
+const contentRepo = require("./content-repo");
+const Filter = require("./filter");
+const SyncApi = require("./sync");
+const MatrixBaseApis = require("./base-apis");
+const MatrixError = httpApi.MatrixError;
+const ContentHelpers = require("./content-helpers");
+const olmlib = require("./crypto/olmlib");
+
+// Disable warnings for now: we use deprecated bluebird functions
+// and need to migrate, but they spam the console with warnings.
+_bluebird2.default.config({ warnings: false });
+
+const SCROLLBACK_DELAY_MS = 3000;
+const CRYPTO_ENABLED = (0, _crypto.isCryptoAvailable)();
+const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
+
+function keysFromRecoverySession(sessions, decryptionKey, roomId) {
+    const keys = [];
+    for (const [sessionId, sessionData] of Object.entries(sessions)) {
+        try {
+            const decrypted = keyFromRecoverySession(sessionData, decryptionKey);
+            decrypted.session_id = sessionId;
+            decrypted.room_id = roomId;
+            keys.push(decrypted);
+        } catch (e) {
+            _logger2.default.log("Failed to decrypt session from backup");
+        }
+    }
+    return keys;
+}
+
+function keyFromRecoverySession(session, decryptionKey) {
+    return JSON.parse(decryptionKey.decrypt(session.session_data.ephemeral, session.session_data.mac, session.session_data.ciphertext));
 }
 
 /**
  * Construct 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.
  * @constructor
  * @extends {external:EventEmitter}
@@ -67,798 +116,1668 @@ try {
  * requests. The value of this property is typically <code>require("request")
  * </code> as it returns a function which meets the required interface. See
  * {@link requestFunction} for more information.
  *
  * @param {string} opts.accessToken The access_token for this user.
  *
  * @param {string} opts.userId The user ID for this user.
  *
- * @param {Object=} opts.store The data store to use. If not specified,
- * this client will not store any HTTP responses.
+ * @param {IdentityServerProvider} [opts.identityServer]
+ * Optional. A provider object with one function `getAccessToken`, which is a
+ * callback that returns a Promise<String> of an identity access token to supply
+ * with identity requests. If the object is unset, no access token will be
+ * supplied.
+ * See also https://github.com/vector-im/riot-web/issues/10615 which seeks to
+ * replace the previous approach of manual access tokens params with this
+ * callback throughout the SDK.
+ *
+ * @param {Object=} opts.store
+ *    The data store used for sync data from the homeserver. If not specified,
+ *    this client will not store any HTTP responses. The `createClient` helper
+ *    will create a default store if needed.
+ *
+ * @param {module:store/session/webstorage~WebStorageSessionStore} opts.sessionStore
+ *    A store to be used for end-to-end crypto session data. Most data has been
+ *    migrated out of here to `cryptoStore` instead. If not specified,
+ *    end-to-end crypto will be disabled. The `createClient` helper
+ *    _will not_ create this store at the moment.
+ *
+ * @param {module:crypto.store.base~CryptoStore} opts.cryptoStore
+ *    A store to be used for end-to-end crypto session data. If not specified,
+ *    end-to-end crypto will be disabled. The `createClient` helper will create
+ *    a default store if needed.
  *
  * @param {string=} opts.deviceId A unique identifier for this device; used for
  *    tracking things like crypto keys and access tokens.  If not specified,
  *    end-to-end crypto will be disabled.
  *
- * @param {Object=} opts.sessionStore A store to be used for end-to-end crypto
- *    session data. This should be a {@link
- *    module:store/session/webstorage~WebStorageSessionStore|WebStorageSessionStore},
- *    or an object implementing the same interface. If not specified,
- *    end-to-end crypto will be disabled.
- *
  * @param {Object} opts.scheduler Optional. The scheduler to use. If not
  * specified, this client will not retry requests on failure. This client
  * will supply its own processing function to
  * {@link module:scheduler~MatrixScheduler#setProcessFunction}.
  *
  * @param {Object} opts.queryParams Optional. Extra query parameters to append
  * to all requests with this client. Useful for application services which require
  * <code>?user_id=</code>.
  *
+ * @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of
+ * time to wait before timing out HTTP requests. If not specified, there is no timeout.
+ *
+ * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
+ * Authorization header instead of query param to send the access token to the server.
+ *
  * @param {boolean} [opts.timelineSupport = false] Set to true to enable
  * improved timeline support ({@link
  * module:client~MatrixClient#getEventTimeline getEventTimeline}). It is
  * disabled by default for compatibility with older clients - in particular to
  * maintain support for back-paginating the live timeline after a '/sync'
  * result with a gap.
+ *
+ * @param {boolean} [opts.unstableClientRelationAggregation = false]
+ * Optional. Set to true to enable client-side aggregation of event relations
+ * via `EventTimelineSet#getRelationsForEvent`.
+ * This feature is currently unstable and the API may change without notice.
+ *
+ * @param {Array} [opts.verificationMethods] Optional. The verification method
+ * that the application can handle.  Each element should be an item from {@link
+ * module:crypto~verificationMethods verificationMethods}, or a class that
+ * implements the {$link module:crypto/verification/Base verifier interface}.
+ *
+ * @param {boolean} [opts.forceTURN]
+ * Optional. Whether relaying calls through a TURN server should be forced.
+ *
+ * @param {boolean} [opts.fallbackICEServerAllowed]
+ * Optional. 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.
  */
 function MatrixClient(opts) {
+    opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl);
+    opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl);
+
     MatrixBaseApis.call(this, opts);
 
+    this.olmVersion = null; // Populated after initCrypto is done
+
+    this.reEmitter = new _ReEmitter2.default(this);
+
     this.store = opts.store || new StubStore();
 
     this.deviceId = opts.deviceId || null;
 
-    var userId = (opts.userId || null);
+    const userId = opts.userId || null;
     this.credentials = {
-        userId: userId,
+        userId: userId
     };
 
     this.scheduler = opts.scheduler;
     if (this.scheduler) {
-        var self = this;
-        this.scheduler.setProcessFunction(function(eventToSend) {
-            var room = self.getRoom(eventToSend.getRoomId());
+        const self = this;
+        this.scheduler.setProcessFunction(function (eventToSend) {
+            const room = self.getRoom(eventToSend.getRoomId());
             if (eventToSend.status !== EventStatus.SENDING) {
-                _updatePendingEventStatus(room, eventToSend,
-                                          EventStatus.SENDING);
+                _updatePendingEventStatus(room, eventToSend, EventStatus.SENDING);
             }
             return _sendEventHttpRequest(self, eventToSend);
         });
     }
     this.clientRunning = false;
 
     this.callList = {
         // callId: MatrixCall
     };
 
     // try constructing a MatrixCall to see if we are running in an environment
     // which has WebRTC. If we are, listen for and handle m.call.* events.
-    var call = webRtcCall.createNewMatrixCall(this);
+    const call = webRtcCall.createNewMatrixCall(this);
     this._supportsVoip = false;
     if (call) {
         setupCallEventHandler(this);
         this._supportsVoip = true;
     }
     this._syncingRetry = null;
     this._syncApi = null;
     this._peekSync = null;
     this._isGuest = false;
     this._ongoingScrollbacks = {};
     this.timelineSupport = Boolean(opts.timelineSupport);
     this.urlPreviewCache = {};
     this._notifTimelineSet = null;
+    this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
 
     this._crypto = null;
-    if (CRYPTO_ENABLED && opts.sessionStore !== null &&
-            userId !== null && this.deviceId !== null) {
-        this._crypto = new Crypto(
-            this, this,
-            opts.sessionStore,
-            userId, this.deviceId
-        );
-
-        this.olmVersion = Crypto.getOlmVersion();
-    }
+    this._cryptoStore = opts.cryptoStore;
+    this._sessionStore = opts.sessionStore;
+    this._verificationMethods = opts.verificationMethods;
+
+    this._forceTURN = opts.forceTURN || false;
+    this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
+
+    // List of which rooms have encryption enabled: separate from crypto because
+    // we still want to know which rooms are encrypted even if crypto is disabled:
+    // we don't want to start sending unencrypted events to them.
+    this._roomList = new _RoomList2.default(this._cryptoStore);
+
+    // The pushprocessor caches useful things, so keep one and re-use it
+    this._pushProcessor = new PushProcessor(this);
+
+    // Cache of the server's /versions response
+    // TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020
+    this._serverVersionsCache = null;
+
+    this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp }
+
+    // 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.decrypted", event => {
+        const oldActions = event.getPushActions();
+        const actions = this._pushProcessor.actionsForEvent(event);
+        event.setPushActions(actions); // Might as well while we're here
+
+        const room = this.getRoom(event.getRoomId());
+        if (!room) return;
+
+        const currentCount = room.getUnreadNotificationCount("highlight");
+
+        // Ensure the unread counts are kept up to date if the event is encrypted
+        // We also want to make sure that the notification count goes up if we already
+        // have encrypted events to avoid other code from resetting 'highlight' to zero.
+        const oldHighlight = oldActions && oldActions.tweaks ? !!oldActions.tweaks.highlight : false;
+        const newHighlight = actions && actions.tweaks ? !!actions.tweaks.highlight : false;
+        if (oldHighlight !== newHighlight || currentCount > 0) {
+            // TODO: Handle mentions received while the client is offline
+            // See also https://github.com/vector-im/riot-web/issues/9069
+            if (!room.hasUserReadEvent(this.getUserId(), event.getId())) {
+                let newCount = currentCount;
+                if (newHighlight && !oldHighlight) newCount++;
+                if (!newHighlight && oldHighlight) newCount--;
+                room.setUnreadNotificationCount("highlight", newCount);
+
+                // Fix 'Mentions Only' rooms from not having the right badge count
+                const totalCount = room.getUnreadNotificationCount('total');
+                if (totalCount < newCount) {
+                    room.setUnreadNotificationCount('total', newCount);
+                }
+            }
+        }
+    });
+
+    // Like above, we have to listen for read receipts from ourselves in order to
+    // correctly handle notification counts on encrypted rooms.
+    // This fixes https://github.com/vector-im/riot-web/issues/9421
+    this.on("Room.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 => {
+                return Object.keys(content[eid]['m.read']).includes(this.getUserId());
+            }).length > 0;
+
+            if (!isSelf) return;
+
+            // Work backwards to determine how many events are unread. We also set
+            // a limit for how back we'll look to avoid spinning CPU for too long.
+            // If we hit the limit, we assume the count is unchanged.
+            const maxHistory = 20;
+            const events = room.getLiveTimeline().getEvents();
+
+            let highlightCount = 0;
+
+            for (let i = events.length - 1; i >= 0; i--) {
+                if (i === events.length - maxHistory) return; // limit reached
+
+                const event = events[i];
+
+                if (room.hasUserReadEvent(this.getUserId(), event.getId())) {
+                    // If the user has read the event, then the counting is done.
+                    break;
+                }
+
+                highlightCount += this.getPushActionsForEvent(event).tweaks.highlight ? 1 : 0;
+            }
+
+            // Note: we don't need to handle 'total' notifications because the counts
+            // will come from the server.
+            room.setUnreadNotificationCount("highlight", highlightCount);
+        }
+    });
 }
 utils.inherits(MatrixClient, EventEmitter);
 utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
 
 /**
+ * Clear any data out of the persistent stores used by the client.
+ *
+ * @returns {Promise} Promise which resolves when the stores have been cleared.
+ */
+MatrixClient.prototype.clearStores = function () {
+    if (this._clientRunning) {
+        throw new Error("Cannot clear stores while client is running");
+    }
+
+    const promises = [];
+
+    promises.push(this.store.deleteAllData());
+    if (this._cryptoStore) {
+        promises.push(this._cryptoStore.deleteAllData());
+    }
+    return _bluebird2.default.all(promises);
+};
+
+/**
+ * Get the user-id of the logged-in user
+ *
+ * @return {?string} MXID for the logged-in user, or null if not logged in
+ */
+MatrixClient.prototype.getUserId = function () {
+    if (this.credentials && this.credentials.userId) {
+        return this.credentials.userId;
+    }
+    return null;
+};
+
+/**
  * Get the domain for this client's MXID
  * @return {?string} Domain of this MXID
  */
-MatrixClient.prototype.getDomain = function() {
+MatrixClient.prototype.getDomain = function () {
     if (this.credentials && this.credentials.userId) {
         return this.credentials.userId.replace(/^.*?:/, '');
     }
     return null;
 };
 
 /**
  * Get the local part of the current user ID e.g. "foo" in "@foo:bar".
  * @return {?string} The user ID localpart or null.
  */
-MatrixClient.prototype.getUserIdLocalpart = function() {
+MatrixClient.prototype.getUserIdLocalpart = function () {
     if (this.credentials && this.credentials.userId) {
         return this.credentials.userId.split(":")[0].substring(1);
     }
     return null;
 };
 
 /**
  * Get the device ID of this client
  * @return {?string} device ID
  */
-MatrixClient.prototype.getDeviceId = function() {
+MatrixClient.prototype.getDeviceId = function () {
     return this.deviceId;
 };
 
-
 /**
  * Check if the runtime environment supports VoIP calling.
  * @return {boolean} True if VoIP is supported.
  */
-MatrixClient.prototype.supportsVoip = function() {
+MatrixClient.prototype.supportsVoip = function () {
     return this._supportsVoip;
 };
 
 /**
+ * Set whether VoIP calls are forced to use only TURN
+ * candidates. This is the same as the forceTURN option
+ * when creating the client.
+ * @param {bool} forceTURN True to force use of TURN servers
+ */
+MatrixClient.prototype.setForceTURN = function (forceTURN) {
+    this._forceTURN = forceTURN;
+};
+
+/**
  * Get the current sync state.
  * @return {?string} the sync state, which may be null.
  * @see module:client~MatrixClient#event:"sync"
  */
-MatrixClient.prototype.getSyncState = function() {
-    if (!this._syncApi) { return null; }
+MatrixClient.prototype.getSyncState = function () {
+    if (!this._syncApi) {
+        return null;
+    }
     return this._syncApi.getSyncState();
 };
 
 /**
+ * Returns the additional data object associated with
+ * the current sync state, or null if there is no
+ * such data.
+ * Sync errors, if available, are put in the 'error' key of
+ * this object.
+ * @return {?Object}
+ */
+MatrixClient.prototype.getSyncStateData = function () {
+    if (!this._syncApi) {
+        return null;
+    }
+    return this._syncApi.getSyncStateData();
+};
+
+/**
  * Return whether the client is configured for a guest account.
  * @return {boolean} True if this is a guest access_token (or no token is supplied).
  */
-MatrixClient.prototype.isGuest = function() {
+MatrixClient.prototype.isGuest = function () {
     return this._isGuest;
 };
 
 /**
  * Return the provided scheduler, if any.
  * @return {?module:scheduler~MatrixScheduler} The scheduler or null
  */
-MatrixClient.prototype.getScheduler = function() {
+MatrixClient.prototype.getScheduler = function () {
     return this.scheduler;
 };
 
 /**
  * Set whether this client is a guest account. <b>This method is experimental
  * and may change without warning.</b>
  * @param {boolean} isGuest True if this is a guest account.
  */
-MatrixClient.prototype.setGuest = function(isGuest) {
+MatrixClient.prototype.setGuest = function (isGuest) {
     // 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._isGuest = isGuest;
 };
 
 /**
  * Retry a backed off syncing request immediately. This should only be used when
  * the user <b>explicitly</b> attempts to retry their lost connection.
  * @return {boolean} True if this resulted in a request being retried.
  */
-MatrixClient.prototype.retryImmediately = function() {
+MatrixClient.prototype.retryImmediately = function () {
     return this._syncApi.retryImmediately();
 };
 
 /**
  * Return the global notification EventTimelineSet, if any
  *
  * @return {EventTimelineSet} the globl notification EventTimelineSet
  */
-MatrixClient.prototype.getNotifTimelineSet = function() {
+MatrixClient.prototype.getNotifTimelineSet = function () {
     return this._notifTimelineSet;
 };
 
 /**
  * Set the global notification EventTimelineSet
  *
  * @param {EventTimelineSet} notifTimelineSet
  */
-MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
+MatrixClient.prototype.setNotifTimelineSet = function (notifTimelineSet) {
     this._notifTimelineSet = notifTimelineSet;
 };
 
+/**
+ * Gets the capabilities of the homeserver. Always returns an object of
+ * capability keys and their options, which may be empty.
+ * @param {boolean} fresh True to ignore any cached values.
+ * @return {module:client.Promise} Resolves to the capabilities of the homeserver
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixClient.prototype.getCapabilities = function (fresh = false) {
+    const now = new Date().getTime();
+
+    if (this._cachedCapabilities && !fresh) {
+        if (now < this._cachedCapabilities.expiration) {
+            _logger2.default.log("Returning cached capabilities");
+            return _bluebird2.default.resolve(this._cachedCapabilities.capabilities);
+        }
+    }
+
+    // We swallow errors because we need a default object anyhow
+    return this._http.authedRequest(undefined, "GET", "/capabilities").catch(e => {
+        _logger2.default.error(e);
+        return null; // otherwise consume the error
+    }).then(r => {
+        if (!r) 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: capabilities,
+            expiration: now + cacheMs
+        };
+
+        _logger2.default.log("Caching capabilities: ", capabilities);
+        return capabilities;
+    });
+};
+
 // Crypto bits
 // ===========
 
 /**
+ * Initialise support for end-to-end encryption in this client
+ *
+ * You should call this method after creating the matrixclient, but *before*
+ * calling `startClient`, if you want to support end-to-end encryption.
+ *
+ * It will return a Promise which will resolve when the crypto layer has been
+ * successfully initialised.
+ */
+MatrixClient.prototype.initCrypto = async function () {
+    if (!(0, _crypto.isCryptoAvailable)()) {
+        throw new Error(`End-to-end encryption not supported in this js-sdk build: did ` + `you remember to load the olm library?`);
+    }
+
+    if (this._crypto) {
+        _logger2.default.warn("Attempt to re-initialise e2e encryption on MatrixClient");
+        return;
+    }
+
+    if (!this._sessionStore) {
+        // this is temporary, the sessionstore is supposed to be going away
+        throw new Error(`Cannot enable encryption: no sessionStore provided`);
+    }
+    if (!this._cryptoStore) {
+        // the cryptostore is provided by sdk.createClient, so this shouldn't happen
+        throw new Error(`Cannot enable encryption: no cryptoStore provided`);
+    }
+
+    // initialise the list of encrypted rooms (whether or not crypto is enabled)
+    _logger2.default.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 _crypto2.default(this, this._sessionStore, userId, this.deviceId, this.store, this._cryptoStore, this._roomList, this._verificationMethods);
+
+    this.reEmitter.reEmit(crypto, ["crypto.keyBackupFailed", "crypto.keyBackupSessionsRemaining", "crypto.roomKeyRequest", "crypto.roomKeyRequestCancellation", "crypto.warning"]);
+
+    _logger2.default.log("Crypto: initialising crypto object...");
+    await crypto.init();
+
+    this.olmVersion = _crypto2.default.getOlmVersion();
+
+    // if crypto initialisation was successful, tell it to attach its event
+    // handlers.
+    crypto.registerEventHandlers(this);
+    this._crypto = crypto;
+};
+
+/**
  * Is end-to-end crypto enabled for this client.
  * @return {boolean} True if end-to-end is enabled.
  */
-MatrixClient.prototype.isCryptoEnabled = function() {
+MatrixClient.prototype.isCryptoEnabled = function () {
     return this._crypto !== null;
 };
 
-
 /**
  * Get the Ed25519 key for this device
  *
  * @return {?string} base64-encoded ed25519 key. Null if crypto is
  *    disabled.
  */
-MatrixClient.prototype.getDeviceEd25519Key = function() {
+MatrixClient.prototype.getDeviceEd25519Key = function () {
     if (!this._crypto) {
         return null;
     }
     return this._crypto.getDeviceEd25519Key();
 };
 
 /**
- * Upload the device keys to the homeserver and ensure that the
- * homeserver has enough one-time keys.
- * @param {number} maxKeys The maximum number of keys to generate
+ * Upload the device keys to the homeserver.
  * @return {object} A promise that will resolve when the keys are uploaded.
  */
-MatrixClient.prototype.uploadKeys = function(maxKeys) {
+MatrixClient.prototype.uploadKeys = function () {
     if (this._crypto === null) {
         throw new Error("End-to-end encryption disabled");
     }
 
-    return this._crypto.uploadKeys(maxKeys);
+    return this._crypto.uploadDeviceKeys();
 };
 
 /**
  * Download the keys for a list of users and stores the keys in the session
  * store.
  * @param {Array} userIds The users to fetch.
  * @param {bool} forceDownload Always download the keys even if cached.
  *
  * @return {Promise} A promise which resolves to a map userId->deviceId->{@link
  * module:crypto~DeviceInfo|DeviceInfo}.
  */
-MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
+MatrixClient.prototype.downloadKeys = function (userIds, forceDownload) {
     if (this._crypto === null) {
-        return q.reject(new Error("End-to-end encryption disabled"));
+        return _bluebird2.default.reject(new Error("End-to-end encryption disabled"));
     }
     return this._crypto.downloadKeys(userIds, forceDownload);
 };
 
 /**
- * List the stored device keys for a user id
- *
- * @deprecated prefer {@link module:client#getStoredDevicesForUser}
- *
- * @param {string} userId the user to list keys for.
- *
- * @return {object[]} list of devices with "id", "verified", "blocked",
- *    "key", and "display_name" parameters.
- */
-MatrixClient.prototype.listDeviceKeys = function(userId) {
-    if (this._crypto === null) {
-        throw new Error("End-to-end encryption disabled");
-    }
-    return this._crypto.listDeviceKeys(userId);
-};
-
-/**
  * Get the stored device keys for a user id
  *
  * @param {string} userId the user to list keys for.
  *
- * @return {module:crypto-deviceinfo[]} list of devices
+ * @return {Promise<module:crypto-deviceinfo[]>} list of devices
  */
-MatrixClient.prototype.getStoredDevicesForUser = function(userId) {
+MatrixClient.prototype.getStoredDevicesForUser = async function (userId) {
     if (this._crypto === null) {
         throw new Error("End-to-end encryption disabled");
     }
     return this._crypto.getStoredDevicesForUser(userId) || [];
 };
 
-
+/**
+ * Get the stored device key for a user id and device id
+ *
+ * @param {string} userId the user to list keys for.
+ * @param {string} deviceId unique identifier for the device
+ *
+ * @return {Promise<?module:crypto-deviceinfo>} device or null
+ */
+MatrixClient.prototype.getStoredDevice = async function (userId, deviceId) {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+    return this._crypto.getStoredDevice(userId, deviceId) || null;
+};
 
 /**
  * Mark the given device as verified
  *
  * @param {string} userId owner of the device
  * @param {string} deviceId unique identifier for the device
  *
  * @param {boolean=} verified whether to mark the device as verified. defaults
  *   to 'true'.
  *
+ * @returns {Promise}
+ *
  * @fires module:client~event:MatrixClient"deviceVerificationChanged"
  */
-MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified) {
+MatrixClient.prototype.setDeviceVerified = function (userId, deviceId, verified) {
     if (verified === undefined) {
         verified = true;
     }
-    _setDeviceVerification(this, userId, deviceId, verified, null);
+    const prom = _setDeviceVerification(this, userId, deviceId, verified, 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._crypto.checkKeyBackup();
+    }
+    return prom;
 };
 
-
 /**
  * Mark the given device as blocked/unblocked
  *
  * @param {string} userId owner of the device
  * @param {string} deviceId unique identifier for the device
  *
  * @param {boolean=} blocked whether to mark the device as blocked. defaults
  *   to 'true'.
  *
+ * @returns {Promise}
+ *
  * @fires module:client~event:MatrixClient"deviceVerificationChanged"
  */
-MatrixClient.prototype.setDeviceBlocked = function(userId, deviceId, blocked) {
+MatrixClient.prototype.setDeviceBlocked = function (userId, deviceId, blocked) {
     if (blocked === undefined) {
         blocked = true;
     }
-    _setDeviceVerification(this, userId, deviceId, null, blocked);
+    return _setDeviceVerification(this, userId, deviceId, null, blocked);
 };
 
-function _setDeviceVerification(client, userId, deviceId, verified, blocked) {
+/**
+ * Mark the given device as known/unknown
+ *
+ * @param {string} userId owner of the device
+ * @param {string} deviceId unique identifier for the device
+ *
+ * @param {boolean=} known whether to mark the device as known. defaults
+ *   to 'true'.
+ *
+ * @returns {Promise}
+ *
+ * @fires module:client~event:MatrixClient"deviceVerificationChanged"
+ */
+MatrixClient.prototype.setDeviceKnown = function (userId, deviceId, known) {
+    if (known === undefined) {
+        known = true;
+    }
+    return _setDeviceVerification(this, userId, deviceId, null, null, known);
+};
+
+async function _setDeviceVerification(client, userId, deviceId, verified, blocked, known) {
     if (!client._crypto) {
         throw new Error("End-to-End encryption disabled");
     }
-    client._crypto.setDeviceVerification(userId, deviceId, verified, blocked);
-    client.emit("deviceVerificationChanged", userId, deviceId);
+    const dev = await client._crypto.setDeviceVerification(userId, deviceId, verified, blocked, known);
+    client.emit("deviceVerificationChanged", userId, deviceId, dev);
 }
 
 /**
+ * Request a key verification from another user.
+ *
+ * @param {string} userId the user to request verification with
+ * @param {Array} methods array of verification methods to use.  Defaults to
+ *    all known methods
+ * @param {Array} devices array of device IDs to send requests to.  Defaults to
+ *    all devices owned by the user
+ *
+ * @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
+ *    when the request is accepted by the other user
+ */
+MatrixClient.prototype.requestVerification = function (userId, methods, devices) {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+    return this._crypto.requestVerification(userId, methods, devices);
+};
+
+/**
+ * Begin a key verification.
+ *
+ * @param {string} method the verification method to use
+ * @param {string} userId the user to verify keys with
+ * @param {string} deviceId the device to verify
+ *
+ * @returns {module:crypto/verification/Base} a verification object
+ */
+MatrixClient.prototype.beginKeyVerification = function (method, userId, deviceId) {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+    return this._crypto.beginKeyVerification(method, userId, deviceId);
+};
+
+/**
+ * Set the global override for whether the client should ever send encrypted
+ * messages to unverified devices.  This provides the default for rooms which
+ * do not specify a value.
+ *
+ * @param {boolean} value whether to blacklist all unverified devices by default
+ */
+MatrixClient.prototype.setGlobalBlacklistUnverifiedDevices = function (value) {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+    this._crypto.setGlobalBlacklistUnverifiedDevices(value);
+};
+
+/**
+ * @return {boolean} whether to blacklist all unverified devices by default
+ */
+MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function () {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+    return this._crypto.getGlobalBlacklistUnverifiedDevices();
+};
+
+/**
  * Get e2e information on the device that sent an event
  *
  * @param {MatrixEvent} event event to be checked
  *
- * @return {module:crypto/deviceinfo?}
+ * @return {Promise<module:crypto/deviceinfo?>}
  */
-MatrixClient.prototype.getEventSenderDeviceInfo = function(event) {
+MatrixClient.prototype.getEventSenderDeviceInfo = async function (event) {
     if (!this._crypto) {
         return null;
     }
 
     return this._crypto.getEventSenderDeviceInfo(event);
 };
 
 /**
  * Check if the sender of an event is verified
  *
  * @param {MatrixEvent} event event to be checked
  *
  * @return {boolean} true if the sender of this event has been verified using
  * {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}.
  */
-MatrixClient.prototype.isEventSenderVerified = function(event) {
-    var device = this.getEventSenderDeviceInfo(event);
+MatrixClient.prototype.isEventSenderVerified = async function (event) {
+    const device = await this.getEventSenderDeviceInfo(event);
     if (!device) {
         return false;
     }
     return device.isVerified();
 };
 
 /**
+ * Cancel a room key request for this event if one is ongoing and resend the
+ * request.
+ * @param  {MatrixEvent} event event of which to cancel and resend the room
+ *                            key request.
+ * @return {Promise} A promise that will resolve when the key request is queued
+ */
+MatrixClient.prototype.cancelAndResendEventRoomKeyRequest = function (event) {
+    return event.cancelAndResendKeyRequest(this._crypto, this.getUserId());
+};
+
+/**
  * Enable end-to-end encryption for a room.
  * @param {string} roomId The room ID to enable encryption in.
  * @param {object} config The encryption config for the room.
- * @return {Object} A promise that will resolve when encryption is setup.
+ * @return {Promise} A promise that will resolve when encryption is set up.
  */
-MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
+MatrixClient.prototype.setRoomEncryption = function (roomId, config) {
     if (!this._crypto) {
         throw new Error("End-to-End encryption disabled");
     }
-    this._crypto.setRoomEncryption(roomId, config);
-    return q();
+    return this._crypto.setRoomEncryption(roomId, config);
 };
 
 /**
  * Whether encryption is enabled for a room.
  * @param {string} roomId the room id to query.
  * @return {bool} whether encryption is enabled.
  */
-MatrixClient.prototype.isRoomEncrypted = function(roomId) {
-    if (!this._crypto) {
+MatrixClient.prototype.isRoomEncrypted = function (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;
     }
 
-    return this._crypto.isRoomEncrypted(roomId);
+    // 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("m.room.encryption", "");
+    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);
+};
+
+/**
+ * Forces the current outbound group session to be discarded such
+ * that another one will be created next time an event is sent.
+ *
+ * @param {string} roomId The ID of the room to discard the session for
+ *
+ * This should not normally be necessary.
+ */
+MatrixClient.prototype.forceDiscardSession = function (roomId) {
+    if (!this._crypto) {
+        throw new Error("End-to-End encryption disabled");
+    }
+    this._crypto.forceDiscardSession(roomId);
+};
+
+/**
+ * Get a list containing all of the room keys
+ *
+ * This should be encrypted before returning it to the user.
+ *
+ * @return {module:client.Promise} a promise which resolves to a list of
+ *    session export objects
+ */
+MatrixClient.prototype.exportRoomKeys = function () {
+    if (!this._crypto) {
+        return _bluebird2.default.reject(new Error("End-to-end encryption disabled"));
+    }
+    return this._crypto.exportRoomKeys();
+};
+
+/**
+ * Import a list of room keys previously exported by exportRoomKeys
+ *
+ * @param {Object[]} keys a list of session export objects
+ *
+ * @return {module:client.Promise} a promise which resolves when the keys
+ *    have been imported
+ */
+MatrixClient.prototype.importRoomKeys = function (keys) {
+    if (!this._crypto) {
+        throw new Error("End-to-end encryption disabled");
+    }
+    return this._crypto.importRoomKeys(keys);
+};
+
+/**
+ * Force a re-check of the local key backup status against
+ * what's on the server.
+ *
+ * @returns {Object} Object with backup info (as returned by
+ *     getKeyBackupVersion) in backupInfo and
+ *     trust information (as returned by isKeyBackupTrusted)
+ *     in trustInfo.
+ */
+MatrixClient.prototype.checkKeyBackup = function () {
+    return this._crypto.checkKeyBackup();
+};
+
+/**
+ * Get information about the current key backup.
+ * @returns {Promise} Information object from API or null
+ */
+MatrixClient.prototype.getKeyBackupVersion = function () {
+    return this._http.authedRequest(undefined, "GET", "/room_keys/version", undefined, undefined, { prefix: httpApi.PREFIX_UNSTABLE }).then(res => {
+        if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) {
+            const err = "Unknown backup algorithm: " + res.algorithm;
+            return _bluebird2.default.reject(err);
+        } else if (!(typeof res.auth_data === "object") || !res.auth_data.public_key) {
+            const err = "Invalid backup data returned";
+            return _bluebird2.default.reject(err);
+        } else {
+            return res;
+        }
+    }).catch(e => {
+        if (e.errcode === 'M_NOT_FOUND') {
+            return null;
+        } else {
+            throw e;
+        }
+    });
+};
+
+/**
+ * @param {object} info key backup info dict from getKeyBackupVersion()
+ * @return {object} {
+ *     usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
+ *     sigs: [
+ *         valid: [bool],
+ *         device: [DeviceInfo],
+ *     ]
+ * }
+ */
+MatrixClient.prototype.isKeyBackupTrusted = function (info) {
+    return this._crypto.isKeyBackupTrusted(info);
+};
+
+/**
+ * @returns {bool} true if the client is configured to back up keys to
+ *     the server, otherwise false.
+ */
+MatrixClient.prototype.getKeyBackupEnabled = function () {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+    return Boolean(this._crypto.backupKey);
+};
+
+/**
+ * Enable backing up of keys, using data previously returned from
+ * getKeyBackupVersion.
+ *
+ * @param {object} info Backup information object as returned by getKeyBackupVersion
+ */
+MatrixClient.prototype.enableKeyBackup = function (info) {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+
+    this._crypto.backupInfo = info;
+    if (this._crypto.backupKey) this._crypto.backupKey.free();
+    this._crypto.backupKey = new global.Olm.PkEncryption();
+    this._crypto.backupKey.set_recipient_key(info.auth_data.public_key);
+
+    this.emit('crypto.keyBackupStatus', true);
+
+    // There may be keys left over from a partially completed backup, so
+    // schedule a send to check.
+    this._crypto.scheduleKeyBackupSend();
+};
+
+/**
+ * Disable backing up of keys.
+ */
+MatrixClient.prototype.disableKeyBackup = function () {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+
+    this._crypto.backupInfo = null;
+    if (this._crypto.backupKey) this._crypto.backupKey.free();
+    this._crypto.backupKey = null;
+
+    this.emit('crypto.keyBackupStatus', false);
+};
+
+/**
+ * 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 {string} password Passphrase string that can be entered by the user
+ *     when restoring the backup as an alternative to entering the recovery key.
+ *     Optional.
+ *
+ * @returns {Promise<object>} Object that can be passed to createKeyBackupVersion and
+ *     additionally has a 'recovery_key' member with the user-facing recovery key string.
+ */
+MatrixClient.prototype.prepareKeyBackupVersion = async function (password) {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+
+    const decryption = new global.Olm.PkDecryption();
+    try {
+        let publicKey;
+        const authData = {};
+        if (password) {
+            const keyInfo = await (0, _backup_password.keyForNewBackup)(password);
+            publicKey = decryption.init_with_private_key(keyInfo.key);
+            authData.private_key_salt = keyInfo.salt;
+            authData.private_key_iterations = keyInfo.iterations;
+        } else {
+            publicKey = decryption.generate_key();
+        }
+
+        authData.public_key = publicKey;
+
+        return {
+            algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
+            auth_data: authData,
+            recovery_key: (0, _recoverykey.encodeRecoveryKey)(decryption.get_private_key())
+        };
+    } finally {
+        decryption.free();
+    }
 };
 
 /**
- * Decrypt a received event according to the algorithm specified in the event.
+ * Create a new key backup version and enable it, using the information return
+ * from prepareKeyBackupVersion.
  *
- * @param {MatrixClient} client
- * @param {MatrixEvent} event
+ * @param {object} info Info object from prepareKeyBackupVersion
+ * @returns {Promise<object>} Object with 'version' param indicating the version created
  */
-function _decryptEvent(client, event) {
-    if (!client._crypto) {
-        _badEncryptedMessage(event, "**Encryption not enabled**");
-        return;
+MatrixClient.prototype.createKeyBackupVersion = function (info) {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+
+    const data = {
+        algorithm: info.algorithm,
+        auth_data: info.auth_data
+    };
+    return this._crypto._signObject(data.auth_data).then(() => {
+        return this._http.authedRequest(undefined, "POST", "/room_keys/version", undefined, data, { prefix: httpApi.PREFIX_UNSTABLE });
+    }).then(res => {
+        this.enableKeyBackup({
+            algorithm: info.algorithm,
+            auth_data: info.auth_data,
+            version: res.version
+        });
+        return res;
+    });
+};
+
+MatrixClient.prototype.deleteKeyBackupVersion = function (version) {
+    if (this._crypto === null) {
+        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.backupInfo && this._crypto.backupInfo.version === version) {
+        this.disableKeyBackup();
+    }
+
+    const path = utils.encodeUri("/room_keys/version/$version", {
+        $version: version
+    });
+
+    return this._http.authedRequest(undefined, "DELETE", path, undefined, undefined, { prefix: httpApi.PREFIX_UNSTABLE });
+};
+
+MatrixClient.prototype._makeKeyBackupPath = function (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: version };
+    return {
+        path: path,
+        queryData: queryData
+    };
+};
+
+/**
+ * Back up session keys to the homeserver.
+ * @param {string} roomId ID of the room that the keys are for Optional.
+ * @param {string} sessionId ID of the session that the keys are for Optional.
+ * @param {integer} version backup version Optional.
+ * @param {object} data Object keys to send
+ * @return {module:client.Promise} a promise that will resolve when the keys
+ * are uploaded
+ */
+MatrixClient.prototype.sendKeyBackup = function (roomId, sessionId, version, data) {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+
+    const path = this._makeKeyBackupPath(roomId, sessionId, version);
+    return this._http.authedRequest(undefined, "PUT", path.path, path.queryData, data, { prefix: httpApi.PREFIX_UNSTABLE });
+};
+
+/**
+ * Marks all group sessions as needing to be backed up and schedules them to
+ * upload in the background as soon as possible.
+ */
+MatrixClient.prototype.scheduleAllGroupSessionsForBackup = async function () {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+
+    await this._crypto.scheduleAllGroupSessionsForBackup();
+};
+
+/**
+ * Marks all group sessions as needing to be backed up without scheduling
+ * them to upload in the background.
+ * @returns {Promise<int>} Resolves to the number of sessions requiring a backup.
+ */
+MatrixClient.prototype.flagAllGroupSessionsForBackup = function () {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+
+    return this._crypto.flagAllGroupSessionsForBackup();
+};
+
+MatrixClient.prototype.isValidRecoveryKey = function (recoveryKey) {
     try {
-        client._crypto.decryptEvent(event);
+        (0, _recoverykey.decodeRecoveryKey)(recoveryKey);
+        return true;
     } catch (e) {
-        if (!(e instanceof Crypto.DecryptionError)) {
-            throw e;
-        }
-        _badEncryptedMessage(event, "**" + e.message + "**");
-        return;
+        return false;
+    }
+};
+
+MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
+
+MatrixClient.prototype.restoreKeyBackupWithPassword = async function (password, targetRoomId, targetSessionId, backupInfo) {
+    const privKey = await (0, _backup_password.keyForExistingBackup)(backupInfo, password);
+    return this._restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo);
+};
+
+MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function (recoveryKey, targetRoomId, targetSessionId, backupInfo) {
+    const privKey = (0, _recoverykey.decodeRecoveryKey)(recoveryKey);
+    return this._restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo);
+};
+
+MatrixClient.prototype._restoreKeyBackup = function (privKey, targetRoomId, targetSessionId, backupInfo) {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+    let totalKeyCount = 0;
+    let keys = [];
+
+    const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, backupInfo.version);
+
+    const decryption = new global.Olm.PkDecryption();
+    let backupPubKey;
+    try {
+        backupPubKey = decryption.init_with_private_key(privKey);
+    } catch (e) {
+        decryption.free();
+        throw e;
+    }
+
+    // If the pubkey computed from the private data we've been given
+    // doesn't match the one in the auth_data, the user has enetered
+    // a different recovery key / the wrong passphrase.
+    if (backupPubKey !== backupInfo.auth_data.public_key) {
+        return _bluebird2.default.reject({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY });
     }
-}
-
-function _badEncryptedMessage(event, reason) {
-    event.setClearData({
-        type: "m.room.message",
-        content: {
-            msgtype: "m.bad.encrypted",
-            body: reason,
-        },
+
+    return this._http.authedRequest(undefined, "GET", path.path, path.queryData, undefined, { prefix: httpApi.PREFIX_UNSTABLE }).then(res => {
+        if (res.rooms) {
+            for (const [roomId, roomData] of Object.entries(res.rooms)) {
+                if (!roomData.sessions) continue;
+
+                totalKeyCount += Object.keys(roomData.sessions).length;
+                const roomKeys = keysFromRecoverySession(roomData.sessions, decryption, roomId, roomKeys);
+                for (const k of roomKeys) {
+                    k.room_id = roomId;
+                    keys.push(k);
+                }
+            }
+        } else if (res.sessions) {
+            totalKeyCount = Object.keys(res.sessions).length;
+            keys = keysFromRecoverySession(res.sessions, decryption, targetRoomId, keys);
+        } else {
+            totalKeyCount = 1;
+            try {
+                const key = keyFromRecoverySession(res, decryption);
+                key.room_id = targetRoomId;
+                key.session_id = targetSessionId;
+                keys.push(key);
+            } catch (e) {
+                _logger2.default.log("Failed to decrypt session from backup");
+            }
+        }
+
+        return this.importRoomKeys(keys);
+    }).then(() => {
+        return this._crypto.setTrustedBackupPubKey(backupPubKey);
+    }).then(() => {
+        return { total: totalKeyCount, imported: keys.length };
+    }).finally(() => {
+        decryption.free();
     });
-}
+};
+
+MatrixClient.prototype.deleteKeysFromBackup = function (roomId, sessionId, version) {
+    if (this._crypto === null) {
+        throw new Error("End-to-end encryption disabled");
+    }
+
+    const path = this._makeKeyBackupPath(roomId, sessionId, version);
+    return this._http.authedRequest(undefined, "DELETE", path.path, path.queryData, undefined, { prefix: httpApi.PREFIX_UNSTABLE });
+};
+
+// Group ops
+// =========
+// Operations on groups that come down the sync stream (ie. ones the
+// user is a member of or invited to)
+
+/**
+ * Get the group for the given group ID.
+ * This function will return a valid group for any group for which a Group event
+ * has been emitted.
+ * @param {string} groupId The group ID
+ * @return {Group} The Group or null if the group is not known or there is no data store.
+ */
+MatrixClient.prototype.getGroup = function (groupId) {
+    return this.store.getGroup(groupId);
+};
+
+/**
+ * Retrieve all known groups.
+ * @return {Group[]} A list of groups, or an empty list if there is no data store.
+ */
+MatrixClient.prototype.getGroups = function () {
+    return this.store.getGroups();
+};
+
+/**
+ * Get the config for the media repository.
+ * @param {module:client.callback} callback Optional.
+ * @return {module:client.Promise} Resolves with an object containing the config.
+ */
+MatrixClient.prototype.getMediaConfig = function (callback) {
+    return this._http.authedRequest(callback, "GET", "/config", undefined, undefined, {
+        prefix: httpApi.PREFIX_MEDIA_R0
+    });
+};
 
 // Room ops
 // ========
 
 /**
  * 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 {string} roomId The room ID
  * @return {Room} The Room or null if it doesn't exist or there is no data store.
  */
-MatrixClient.prototype.getRoom = function(roomId) {
+MatrixClient.prototype.getRoom = function (roomId) {
     return this.store.getRoom(roomId);
 };
 
 /**
  * Retrieve all known rooms.
  * @return {Room[]} A list of rooms, or an empty list if there is no data store.
  */
-MatrixClient.prototype.getRooms = function() {
+MatrixClient.prototype.getRooms = function () {
     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.
+ * @return {Room[]} A list of rooms, or an empty list if there is no data store.
+ */
+MatrixClient.prototype.getVisibleRooms = function () {
+    const allRooms = this.store.getRooms();
+
+    const replacedRooms = new Set();
+    for (const r of allRooms) {
+        const createEvent = r.currentState.getStateEvents('m.room.create', '');
+        // invites are included in this list and we don't know their create events yet
+        if (createEvent) {
+            const predecessor = createEvent.getContent()['predecessor'];
+            if (predecessor && predecessor['room_id']) {
+                replacedRooms.add(predecessor['room_id']);
+            }
+        }
+    }
+
+    return allRooms.filter(r => {
+        const tombstone = r.currentState.getStateEvents('m.room.tombstone', '');
+        if (tombstone && replacedRooms.has(r.roomId)) {
+            return false;
+        }
+        return true;
+    });
+};
+
+/**
  * Retrieve a user.
  * @param {string} userId The user ID to retrieve.
  * @return {?User} A user or null if there is no data store or the user does
  * not exist.
  */
-MatrixClient.prototype.getUser = function(userId) {
+MatrixClient.prototype.getUser = function (userId) {
     return this.store.getUser(userId);
 };
 
 /**
  * Retrieve all known users.
  * @return {User[]} A list of users, or an empty list if there is no data store.
  */
-MatrixClient.prototype.getUsers = function() {
+MatrixClient.prototype.getUsers = function () {
     return this.store.getUsers();
 };
 
 // User Account Data operations
 // ============================
 
 /**
  * Set account data event for the current user.
  * @param {string} eventType The event type
- * @param {Object} content the contents object for the event
+ * @param {Object} contents the contents object for the event
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.setAccountData = function(eventType, contents, callback) {
-    var path = utils.encodeUri("/user/$userId/account_data/$type", {
+MatrixClient.prototype.setAccountData = function (eventType, contents, callback) {
+    const path = utils.encodeUri("/user/$userId/account_data/$type", {
         $userId: this.credentials.userId,
-        $type: eventType,
+        $type: eventType
     });
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, contents
-    );
+    return this._http.authedRequest(callback, "PUT", path, undefined, contents);
 };
 
 /**
  * Get account data event of given type for the current user.
  * @param {string} eventType The event type
- * @param {module:client.callback} callback Optional.
  * @return {?object} The contents of the given account data event
  */
-MatrixClient.prototype.getAccountData = function(eventType) {
+MatrixClient.prototype.getAccountData = function (eventType) {
     return this.store.getAccountData(eventType);
 };
 
+/**
+ * Gets the users that are ignored by this client
+ * @returns {string[]} The array of users that are ignored (empty if none)
+ */
+MatrixClient.prototype.getIgnoredUsers = function () {
+    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 {string[]} userIds the user IDs to ignore
+ * @param {module:client.callback} [callback] Optional.
+ * @return {module:client.Promise} Resolves: Account data event
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixClient.prototype.setIgnoredUsers = function (userIds, callback) {
+    const content = { ignored_users: {} };
+    userIds.map(u => content.ignored_users[u] = {});
+    return this.setAccountData("m.ignored_user_list", content, callback);
+};
+
+/**
+ * Gets whether or not a specific user is being ignored by this client.
+ * @param {string} userId the user ID to check
+ * @returns {boolean} true if the user is ignored, false otherwise
+ */
+MatrixClient.prototype.isUserIgnored = function (userId) {
+    return this.getIgnoredUsers().indexOf(userId) !== -1;
+};
+
 // Room operations
 // ===============
 
 /**
  * Join a room. If you have already joined the room, this will no-op.
  * @param {string} roomIdOrAlias The room ID or room alias to join.
  * @param {Object} opts Options when joining the room.
  * @param {boolean} opts.syncRoom True to do a room initial sync on the resulting
  * room. If false, the <strong>returned Room object will have no current state.
  * </strong> Default: true.
  * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite,
  *                                     the signing URL is passed in this parameter.
+ * @param {string[]} opts.viaServers The server names to try and join through in
+ *                                   addition to those that are automatically chosen.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: Room object.
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
+MatrixClient.prototype.joinRoom = function (roomIdOrAlias, opts, callback) {
     // to help people when upgrading..
     if (utils.isFunction(opts)) {
         throw new Error("Expected 'opts' object, got function.");
     }
     opts = opts || {};
-    if (opts.syncRoom === undefined) { opts.syncRoom = true; }
-
-    var room = this.getRoom(roomIdOrAlias);
+    if (opts.syncRoom === undefined) {
+        opts.syncRoom = true;
+    }
+
+    const room = this.getRoom(roomIdOrAlias);
     if (room && room.hasMembershipState(this.credentials.userId, "join")) {
-        return q(room);
+        return _bluebird2.default.resolve(room);
     }
 
-    var sign_promise = q();
+    let sign_promise = _bluebird2.default.resolve();
 
     if (opts.inviteSignUrl) {
-        sign_promise = this._http.requestOtherUrl(
-            undefined, 'POST',
-            opts.inviteSignUrl, { mxid: this.credentials.userId }
-        );
+        sign_promise = this._http.requestOtherUrl(undefined, 'POST', opts.inviteSignUrl, { mxid: this.credentials.userId });
+    }
+
+    const queryString = {};
+    if (opts.viaServers) {
+        queryString["server_name"] = opts.viaServers;
     }
 
-    var defer = q.defer();
-
-    var self = this;
-    sign_promise.then(function(signed_invite_object) {
-        var data = {};
+    const reqOpts = { qsStringifyOptions: { arrayFormat: 'repeat' } };
+
+    const defer = _bluebird2.default.defer();
+
+    const self = this;
+    sign_promise.then(function (signed_invite_object) {
+        const data = {};
         if (signed_invite_object) {
             data.third_party_signed = signed_invite_object;
         }
 
-        var path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias});
-        return self._http.authedRequest(undefined, "POST", path, undefined, data);
-    }).then(function(res) {
-        var roomId = res.room_id;
-        var syncApi = new SyncApi(self, self._clientOpts);
-        var room = syncApi.createRoom(roomId);
+        const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias });
+        return self._http.authedRequest(undefined, "POST", path, queryString, data, reqOpts);
+    }).then(function (res) {
+        const roomId = res.room_id;
+        const syncApi = new SyncApi(self, self._clientOpts);
+        const room = syncApi.createRoom(roomId);
         if (opts.syncRoom) {
             // v2 will do this for us
             // return syncApi.syncRoom(room);
         }
-        return q(room);
-    }).done(function(room) {
+        return _bluebird2.default.resolve(room);
+    }).done(function (room) {
         _resolve(callback, defer, room);
-    }, function(err) {
+    }, function (err) {
         _reject(callback, defer, err);
     });
     return defer.promise;
 };
 
 /**
  * Resend an event.
  * @param {MatrixEvent} event The event to resend.
  * @param {Room} room Optional. The room the event is in. Will update the
  * timeline entry if provided.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.resendEvent = function(event, room) {
+MatrixClient.prototype.resendEvent = function (event, room) {
     _updatePendingEventStatus(room, event, EventStatus.SENDING);
     return _sendEvent(this, room, event);
 };
 
 /**
  * Cancel a queued or unsent event.
  *
  * @param {MatrixEvent} event   Event to cancel
  * @throws Error if the event is not in QUEUED or NOT_SENT state
  */
-MatrixClient.prototype.cancelPendingEvent = function(event) {
+MatrixClient.prototype.cancelPendingEvent = function (event) {
     if ([EventStatus.QUEUED, EventStatus.NOT_SENT].indexOf(event.status) < 0) {
         throw new Error("cannot cancel an event with status " + event.status);
     }
 
     // first tell the scheduler to forget about it, if it's queued
     if (this.scheduler) {
         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.
-    var room = this.getRoom(event.getRoomId());
+    const room = this.getRoom(event.getRoomId());
     _updatePendingEventStatus(room, event, EventStatus.CANCELLED);
 };
 
 /**
  * @param {string} roomId
  * @param {string} name
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.setRoomName = function(roomId, name, callback) {
-    return this.sendStateEvent(roomId, "m.room.name", {name: name},
-                               undefined, callback);
+MatrixClient.prototype.setRoomName = function (roomId, name, callback) {
+    return this.sendStateEvent(roomId, "m.room.name", { name: name }, undefined, callback);
 };
 
 /**
  * @param {string} roomId
  * @param {string} topic
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.setRoomTopic = function(roomId, topic, callback) {
-    return this.sendStateEvent(roomId, "m.room.topic", {topic: topic},
-                               undefined, callback);
+MatrixClient.prototype.setRoomTopic = function (roomId, topic, callback) {
+    return this.sendStateEvent(roomId, "m.room.topic", { topic: topic }, undefined, callback);
 };
 
 /**
  * @param {string} roomId
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.getRoomTags = function(roomId, callback) {
-    var path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/", {
+MatrixClient.prototype.getRoomTags = function (roomId, callback) {
+    const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/", {
         $userId: this.credentials.userId,
-        $roomId: roomId,
+        $roomId: roomId
     });
-    return this._http.authedRequest(
-        callback, "GET", path, undefined
-    );
+    return this._http.authedRequest(callback, "GET", path, undefined);
 };
 
 /**
  * @param {string} roomId
  * @param {string} tagName name of room tag to be set
  * @param {object} metadata associated with that tag to be stored
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.setRoomTag = function(roomId, tagName, metadata, callback) {
-    var path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
+MatrixClient.prototype.setRoomTag = function (roomId, tagName, metadata, callback) {
+    const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
         $userId: this.credentials.userId,
         $roomId: roomId,
-        $tag: tagName,
+        $tag: tagName
     });
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, metadata
-    );
+    return this._http.authedRequest(callback, "PUT", path, undefined, metadata);
 };
 
 /**
  * @param {string} roomId
  * @param {string} tagName name of room tag to be removed
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.deleteRoomTag = function(roomId, tagName, callback) {
-    var path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
+MatrixClient.prototype.deleteRoomTag = function (roomId, tagName, callback) {
+    const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
         $userId: this.credentials.userId,
         $roomId: roomId,
-        $tag: tagName,
+        $tag: tagName
     });
-    return this._http.authedRequest(
-        callback, "DELETE", path, undefined, undefined
-    );
+    return this._http.authedRequest(callback, "DELETE", path, undefined, undefined);
 };
 
 /**
  * @param {string} roomId
  * @param {string} eventType event type to be set
  * @param {object} content event content
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.setRoomAccountData = function(roomId, eventType,
-                                                     content, callback) {
-    var path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", {
+MatrixClient.prototype.setRoomAccountData = function (roomId, eventType, content, callback) {
+    const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", {
         $userId: this.credentials.userId,
         $roomId: roomId,
-        $type: eventType,
+        $type: eventType
     });
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, content
-    );
+    return this._http.authedRequest(callback, "PUT", path, undefined, content);
 };
 
 /**
  * Set a user's power level.
  * @param {string} roomId
  * @param {string} userId
  * @param {Number} powerLevel
  * @param {MatrixEvent} event
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.setPowerLevel = function(roomId, userId, powerLevel,
-                                                event, callback) {
-    var content = {
+MatrixClient.prototype.setPowerLevel = function (roomId, userId, powerLevel, event, callback) {
+    let content = {
         users: {}
     };
     if (event && event.getType() === "m.room.power_levels") {
         // 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());
     }
     content.users[userId] = powerLevel;
-    var path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", {
+    const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", {
         $roomId: roomId
     });
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, content
-    );
+    return this._http.authedRequest(callback, "PUT", path, undefined, content);
 };
 
 /**
  * @param {string} roomId
  * @param {string} eventType
  * @param {Object} content
  * @param {string} txnId Optional.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
-                                            callback) {
-    if (utils.isFunction(txnId)) { callback = txnId; txnId = undefined; }
+MatrixClient.prototype.sendEvent = function (roomId, eventType, content, txnId, callback) {
+    return this._sendCompleteEvent(roomId, {
+        type: eventType,
+        content: content
+    }, txnId, callback);
+};
+/**
+ * @param {string} roomId
+ * @param {object} 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 {string} txnId the txnId.
+ * @param {module:client.callback} callback Optional.
+ * @return {module:client.Promise} Resolves: TODO
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixClient.prototype._sendCompleteEvent = function (roomId, eventObject, txnId, callback) {
+    if (utils.isFunction(txnId)) {
+        callback = txnId;txnId = undefined;
+    }
 
     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.
-    var room = this.getRoom(roomId);
-    var localEvent = new MatrixEvent({
+    const localEvent = new MatrixEvent(Object.assign(eventObject, {
         event_id: "~" + roomId + ":" + txnId,
         user_id: this.credentials.userId,
         room_id: roomId,
-        type: eventType,
-        origin_server_ts: new Date().getTime(),
-        content: content
-    });
+        origin_server_ts: new Date().getTime()
+    }));
+
+    const room = this.getRoom(roomId);
+
+    // 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 && targetId.startsWith("~")) {
+        const target = room.getPendingEvents().find(e => e.getId() === targetId);
+        target.once("Event.localEventIdReplaced", () => {
+            localEvent.updateAssociatedId(target.getId());
+        });
+    }
+
+    const type = localEvent.getType();
+    _logger2.default.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
+
     localEvent._txnId = txnId;
-    localEvent.status = EventStatus.SENDING;
+    localEvent.setStatus(EventStatus.SENDING);
 
     // add this event immediately to the local store as 'sending'.
     if (room) {
         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 === EventStatus.NOT_SENT) {
+        return _bluebird2.default.reject(new Error("Event blocked by other events not yet sent"));
+    }
+
     return _sendEvent(this, room, localEvent, callback);
 };
 
-
 // encrypts the event if necessary
 // adds the event to the queue, or sends it
 // marks the event as sent/unsent
 // returns a promise which resolves with the result of the send request
 function _sendEvent(client, room, event, callback) {
-    // Add an extra q() to turn synchronous exceptions into promise rejections,
+    // 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 q().then(function() {
-        var encryptionPromise = null;
-        if (client._crypto) {
-            encryptionPromise = client._crypto.encryptEventIfNeeded(event, room);
+    return _bluebird2.default.resolve().then(function () {
+        const encryptionPromise = _encryptEventIfNeeded(client, event, room);
+
+        if (!encryptionPromise) {
+            return null;
         }
-        if (encryptionPromise) {
-            _updatePendingEventStatus(room, event, EventStatus.ENCRYPTING);
-            encryptionPromise = encryptionPromise.then(function() {
-                _updatePendingEventStatus(room, event, EventStatus.SENDING);
-            });
-        }
-        return encryptionPromise;
-    }).then(function() {
-        var promise;
+
+        _updatePendingEventStatus(room, event, EventStatus.ENCRYPTING);
+        return encryptionPromise.then(() => {
+            _updatePendingEventStatus(room, event, EventStatus.SENDING);
+        });
+    }).then(function () {
+        let promise;
         // this event may be queued
         if (client.scheduler) {
             // if this returns a promsie 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 = client.scheduler.queueEvent(event);
             if (promise && client.scheduler.getQueueForEvent(event).length > 1) {
@@ -867,478 +1786,751 @@ function _sendEvent(client, room, event,
                 _updatePendingEventStatus(room, event, EventStatus.QUEUED);
             }
         }
 
         if (!promise) {
             promise = _sendEventHttpRequest(client, event);
         }
         return promise;
-    }).then(function(res) {  // the request was sent OK
+    }).then(function (res) {
+        // the request was sent OK
         if (room) {
             room.updatePendingEvent(event, EventStatus.SENT, res.event_id);
         }
         if (callback) {
             callback(null, res);
         }
         return res;
-    }, function(err) {
+    }, function (err) {
         // the request failed to send.
-        console.error("Error sending event", err.stack || err);
+        _logger2.default.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;
             _updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
+            // also put the event object on the error: the caller will need this
+            // to resend or cancel the event
+            err.event = event;
 
             if (callback) {
                 callback(err);
             }
         } catch (err2) {
-            console.error("Exception in error handler!", err2.stack || err);
+            _logger2.default.error("Exception in error handler!", err2.stack || err);
         }
         throw err;
     });
 }
 
+/**
+ * Encrypt an event according to the configuration of the room, if necessary.
+ *
+ * @param {MatrixClient} client
+ *
+ * @param {module:models/event.MatrixEvent} event  event to be sent
+ *
+ * @param {module:models/room?} room destination room. Null if the destination
+ *     is not a room we have seen over the sync pipe.
+ *
+ * @return {module:client.Promise?} Promise which resolves when the event has been
+ *     encrypted, or null if nothing was needed
+ */
+
+function _encryptEventIfNeeded(client, 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 (!client.isRoomEncrypted(event.getRoomId())) {
+        // looks like this room isn't encrypted.
+        return null;
+    }
+
+    if (event.getType() === "m.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 (!client._crypto) {
+        throw new Error("This room is configured to use encryption, but your client does " + "not support encryption.");
+    }
+
+    return client._crypto.encryptEvent(event, room);
+}
+/**
+ * Returns the eventType that should be used taking encryption into account
+ * for a given eventType.
+ * @param {MatrixClient} client the client
+ * @param {string} roomId the room for the events `eventType` relates to
+ * @param {string} eventType the event type
+ * @return {string} the event type taking encryption into account
+ */
+function _getEncryptedIfNeededEventType(client, roomId, eventType) {
+    if (eventType === "m.reaction") {
+        return eventType;
+    }
+    const isEncrypted = client.isRoomEncrypted(roomId);
+    return isEncrypted ? "m.room.encrypted" : eventType;
+}
+
 function _updatePendingEventStatus(room, event, newStatus) {
     if (room) {
         room.updatePendingEvent(event, newStatus);
     } else {
-        event.status = newStatus;
+        event.setStatus(newStatus);
     }
 }
 
 function _sendEventHttpRequest(client, event) {
-    var txnId = event._txnId ? event._txnId : client.makeTxnId();
-
-    var pathParams = {
+    const txnId = event._txnId ? event._txnId : client.makeTxnId();
+
+    const pathParams = {
         $roomId: event.getRoomId(),
         $eventType: event.getWireType(),
         $stateKey: event.getStateKey(),
-        $txnId: txnId,
+        $txnId: txnId
     };
 
-    var path;
+    let path;
 
     if (event.isState()) {
-        var pathTemplate = "/rooms/$roomId/state/$eventType";
+        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 {
-        path = utils.encodeUri(
-            "/rooms/$roomId/send/$eventType/$txnId", pathParams
-        );
+    } else if (event.isRedaction()) {
+        const pathTemplate = `/rooms/$roomId/redact/$redactsEventId/$txnId`;
+        path = utils.encodeUri(pathTemplate, Object.assign({
+            $redactsEventId: event.event.redacts
+        }, pathParams));
+    } else {
+        path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", pathParams);
     }
 
-    return client._http.authedRequest(
-        undefined, "PUT", path, undefined, event.getWireContent()
-    );
+    return client._http.authedRequest(undefined, "PUT", path, undefined, event.getWireContent()).then(res => {
+        _logger2.default.log(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`);
+        return res;
+    });
 }
 
 /**
  * @param {string} roomId
+ * @param {string} eventId
+ * @param {string} [txnId]  transaction id. One will be made up if not
+ *    supplied.
+ * @param {module:client.callback} callback Optional.
+ * @return {module:client.Promise} Resolves: TODO
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixClient.prototype.redactEvent = function (roomId, eventId, txnId, callback) {
+    return this._sendCompleteEvent(roomId, {
+        type: "m.room.redaction",
+        content: {},
+        redacts: eventId
+    }, txnId, callback);
+};
+
+/**
+ * @param {string} roomId
  * @param {Object} content
  * @param {string} txnId Optional.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.sendMessage = function(roomId, content, txnId, callback) {
-    if (utils.isFunction(txnId)) { callback = txnId; txnId = undefined; }
-    return this.sendEvent(
-        roomId, "m.room.message", content, txnId, callback
-    );
+MatrixClient.prototype.sendMessage = function (roomId, content, txnId, callback) {
+    if (utils.isFunction(txnId)) {
+        callback = txnId;txnId = undefined;
+    }
+    return this.sendEvent(roomId, "m.room.message", content, txnId, callback);
 };
 
 /**
  * @param {string} roomId
  * @param {string} body
  * @param {string} txnId Optional.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.sendTextMessage = function(roomId, body, txnId, callback) {
-    var content = {
-         msgtype: "m.text",
-         body: body
-    };
+MatrixClient.prototype.sendTextMessage = function (roomId, body, txnId, callback) {
+    const content = ContentHelpers.makeTextMessage(body);
     return this.sendMessage(roomId, content, txnId, callback);
 };
 
 /**
  * @param {string} roomId
  * @param {string} body
  * @param {string} txnId Optional.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.sendNotice = function(roomId, body, txnId, callback) {
-    var content = {
-         msgtype: "m.notice",
-         body: body
-    };
+MatrixClient.prototype.sendNotice = function (roomId, body, txnId, callback) {
+    const content = ContentHelpers.makeNotice(body);
     return this.sendMessage(roomId, content, txnId, callback);
 };
 
 /**
  * @param {string} roomId
  * @param {string} body
  * @param {string} txnId Optional.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.sendEmoteMessage = function(roomId, body, txnId, callback) {
-    var content = {
-         msgtype: "m.emote",
-         body: body
-    };
+MatrixClient.prototype.sendEmoteMessage = function (roomId, body, txnId, callback) {
+    const content = ContentHelpers.makeEmoteMessage(body);
     return this.sendMessage(roomId, content, txnId, callback);
 };
 
 /**
  * @param {string} roomId
  * @param {string} url
  * @param {Object} info
  * @param {string} text
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.sendImageMessage = function(roomId, url, info, text, callback) {
-    if (utils.isFunction(text)) { callback = text; text = undefined; }
-    if (!text) { text = "Image"; }
-    var content = {
-         msgtype: "m.image",
-         url: url,
-         info: info,
-         body: text
+MatrixClient.prototype.sendImageMessage = function (roomId, url, info, text, callback) {
+    if (utils.isFunction(text)) {
+        callback = text;text = undefined;
+    }
+    if (!text) {
+        text = "Image";
+    }
+    const content = {
+        msgtype: "m.image",
+        url: url,
+        info: info,
+        body: text
     };
     return this.sendMessage(roomId, content, callback);
 };
 
 /**
  * @param {string} roomId
+ * @param {string} url
+ * @param {Object} info
+ * @param {string} text
+ * @param {module:client.callback} callback Optional.
+ * @return {module:client.Promise} Resolves: TODO
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixClient.prototype.sendStickerMessage = function (roomId, url, info, text, callback) {
+    if (utils.isFunction(text)) {
+        callback = text;text = undefined;
+    }
+    if (!text) {
+        text = "Sticker";
+    }
+    const content = {
+        url: url,
+        info: info,
+        body: text
+    };
+    return this.sendEvent(roomId, "m.sticker", content, callback, undefined);
+};
+
+/**
+ * @param {string} roomId
+ * @param {string} body
+ * @param {string} htmlBody
+ * @param {module:client.callback} callback Optional.
+ * @return {module:client.Promise} Resolves: TODO
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixClient.prototype.sendHtmlMessage = function (roomId, body, htmlBody, callback) {
+    const content = ContentHelpers.makeHtmlMessage(body, htmlBody);
+    return this.sendMessage(roomId, content, callback);
+};
+
+/**
+ * @param {string} roomId
  * @param {string} body
  * @param {string} htmlBody
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.sendHtmlMessage = function(roomId, body, htmlBody, callback) {
-    var content = {
-        msgtype: "m.text",
-        format: "org.matrix.custom.html",
-        body: body,
-        formatted_body: htmlBody
-    };
+MatrixClient.prototype.sendHtmlNotice = function (roomId, body, htmlBody, callback) {
+    const content = ContentHelpers.makeHtmlNotice(body, htmlBody);
     return this.sendMessage(roomId, content, callback);
 };
 
 /**
  * @param {string} roomId
  * @param {string} body
  * @param {string} htmlBody
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.sendHtmlNotice = function(roomId, body, htmlBody, callback) {
-    var content = {
-        msgtype: "m.notice",
-        format: "org.matrix.custom.html",
-        body: body,
-        formatted_body: htmlBody
-    };
-    return this.sendMessage(roomId, content, callback);
-};
-
-/**
- * @param {string} roomId
- * @param {string} body
- * @param {string} htmlBody
- * @param {module:client.callback} callback Optional.
- * @return {module:client.Promise} Resolves: TODO
- * @return {module:http-api.MatrixError} Rejects: with an error response.
- */
-MatrixClient.prototype.sendHtmlEmote = function(roomId, body, htmlBody, callback) {
-    var content = {
-        msgtype: "m.emote",
-        format: "org.matrix.custom.html",
-        body: body,
-        formatted_body: htmlBody
-    };
+MatrixClient.prototype.sendHtmlEmote = function (roomId, body, htmlBody, callback) {
+    const content = ContentHelpers.makeHtmlEmote(body, htmlBody);
     return this.sendMessage(roomId, content, callback);
 };
 
 /**
  * Send a receipt.
  * @param {Event} event The event being acknowledged
  * @param {string} receiptType The kind of receipt e.g. "m.read"
+ * @param {object} opts Additional content to send alongside the receipt.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.sendReceipt = function(event, receiptType, callback) {
+MatrixClient.prototype.sendReceipt = function (event, receiptType, opts, callback) {
+    if (typeof opts === 'function') {
+        callback = opts;
+        opts = {};
+    }
+
     if (this.isGuest()) {
-        return q({}); // guests cannot send receipts so don't bother.
+        return _bluebird2.default.resolve({}); // guests cannot send receipts so don't bother.
     }
 
-    var path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
+    const path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
         $roomId: event.getRoomId(),
         $receiptType: receiptType,
         $eventId: event.getId()
     });
-    var promise = this._http.authedRequest(
-        callback, "POST", path, undefined, {}
-    );
-
-    var room = this.getRoom(event.getRoomId());
+    const promise = this._http.authedRequest(callback, "POST", path, undefined, opts || {});
+
+    const room = this.getRoom(event.getRoomId());
     if (room) {
         room._addLocalEchoReceipt(this.credentials.userId, event, receiptType);
     }
     return promise;
 };
 
 /**
  * Send a read receipt.
  * @param {Event} event The event that has been read.
+ * @param {object} opts The options for the read receipt.
+ * @param {boolean} opts.hidden True to prevent the receipt from being sent to
+ * other users and homeservers. Default false (send to everyone). <b>This
+ * property is unstable and may change in the future.</b>
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.sendReadReceipt = function(event, callback) {
-    return this.sendReceipt(event, "m.read", callback);
+MatrixClient.prototype.sendReadReceipt = async function (event, opts, callback) {
+    if (typeof opts === 'function') {
+        callback = opts;
+        opts = {};
+    }
+    if (!opts) opts = {};
+
+    const eventId = event.getId();
+    const room = this.getRoom(event.getRoomId());
+    if (room && room.hasPendingEvent(eventId)) {
+        throw new Error(`Cannot set read receipt to a pending event (${eventId})`);
+    }
+
+    const addlContent = {
+        "m.hidden": Boolean(opts.hidden)
+    };
+
+    return this.sendReceipt(event, "m.read", addlContent, callback);
 };
 
+/**
+ * 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 {string} roomId ID of the room that has been read
+ * @param {string} rmEventId ID of the event that has been read
+ * @param {string} 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 {object} opts Options for the read markers
+ * @param {object} opts.hidden True to hide the receipt from other users and homeservers.
+ * <b>This property is unstable and may change in the future.</b>
+ * @return {module:client.Promise} Resolves: the empty object, {}.
+ */
+MatrixClient.prototype.setRoomReadMarkers = async function (roomId, rmEventId, rrEvent, opts) {
+    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 && room.hasPendingEvent(rrEventId)) {
+            throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`);
+        }
+        if (room) {
+            room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read");
+        }
+    }
+
+    return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, opts);
+};
 
 /**
  * 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 {string} url The URL to get preview data for
  * @param {Number} 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.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: Object of OG metadata.
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  * May return synthesized attributes if the URL lacked OG meta.
  */
-MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
-    var key = ts + "_" + url;
-    var og = this.urlPreviewCache[key];
+MatrixClient.prototype.getUrlPreview = function (url, ts, callback) {
+    const key = ts + "_" + url;
+    const og = this.urlPreviewCache[key];
     if (og) {
-        return q(og);
+        return _bluebird2.default.resolve(og);
     }
 
-    var self = this;
-    return this._http.authedRequestWithPrefix(
-        callback, "GET", "/preview_url", {
-            url: url,
-            ts: ts,
-        }, undefined, httpApi.PREFIX_MEDIA_R0
-    ).then(function(response) {
+    const self = this;
+    return this._http.authedRequest(callback, "GET", "/preview_url", {
+        url: url,
+        ts: ts
+    }, undefined, {
+        prefix: httpApi.PREFIX_MEDIA_R0
+    }).then(function (response) {
         // TODO: expire cache occasionally
         self.urlPreviewCache[key] = response;
         return response;
     });
 };
 
 /**
  * @param {string} roomId
  * @param {boolean} isTyping
  * @param {Number} timeoutMs
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.sendTyping = function(roomId, isTyping, timeoutMs, callback) {
+MatrixClient.prototype.sendTyping = function (roomId, isTyping, timeoutMs, callback) {
     if (this.isGuest()) {
-        return q({}); // guests cannot send typing notifications so don't bother.
+        return _bluebird2.default.resolve({}); // guests cannot send typing notifications so don't bother.
     }
 
-    var path = utils.encodeUri("/rooms/$roomId/typing/$userId", {
+    const path = utils.encodeUri("/rooms/$roomId/typing/$userId", {
         $roomId: roomId,
         $userId: this.credentials.userId
     });
-    var data = {
+    const data = {
         typing: isTyping
     };
     if (isTyping) {
         data.timeout = timeoutMs ? timeoutMs : 20000;
     }
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, data
-    );
+    return this._http.authedRequest(callback, "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 {string} roomId The room ID to search from
+ * @param {boolean} 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.
+ * @return {Room[]} An array of rooms representing the upgrade
+ * history.
+ */
+MatrixClient.prototype.getRoomUpgradeHistory = function (roomId, verifyLinks = false) {
+    let currentRoom = this.getRoom(roomId);
+    if (!currentRoom) return [];
+
+    const upgradeHistory = [currentRoom];
+
+    // Work backwards first, looking at create events.
+    let createEvent = currentRoom.currentState.getStateEvents("m.room.create", "");
+    while (createEvent) {
+        _logger2.default.log(`Looking at ${createEvent.getId()}`);
+        const predecessor = createEvent.getContent()['predecessor'];
+        if (predecessor && predecessor['room_id']) {
+            _logger2.default.log(`Looking at predecessor ${predecessor['room_id']}`);
+            const refRoom = this.getRoom(predecessor['room_id']);
+            if (!refRoom) break; // end of the chain
+
+            if (verifyLinks) {
+                const tombstone = refRoom.currentState.getStateEvents("m.room.tombstone", "");
+
+                if (!tombstone || tombstone.getContent()['replacement_room'] !== refRoom.roomId) {
+                    break;
+                }
+            }
+
+            // Insert at the front because we're working backwards from the currentRoom
+            upgradeHistory.splice(0, 0, refRoom);
+            createEvent = refRoom.currentState.getStateEvents("m.room.create", "");
+        } else {
+            // No further create events to look at
+            break;
+        }
+    }
+
+    // Work forwards next, looking at tombstone events
+    let tombstoneEvent = currentRoom.currentState.getStateEvents("m.room.tombstone", "");
+    while (tombstoneEvent) {
+        const refRoom = this.getRoom(tombstoneEvent.getContent()['replacement_room']);
+        if (!refRoom) break; // end of the chain
+        if (refRoom.roomId === currentRoom.roomId) break; // Tombstone is referencing it's own room
+
+        if (verifyLinks) {
+            createEvent = refRoom.currentState.getStateEvents("m.room.create", "");
+            if (!createEvent || !createEvent.getContent()['predecessor']) break;
+
+            const predecessor = createEvent.getContent()['predecessor'];
+            if (predecessor['room_id'] !== currentRoom.roomId) break;
+        }
+
+        // Push to the end because we're looking forwards
+        upgradeHistory.push(refRoom);
+        const roomIds = new Set(upgradeHistory.map(ref => ref.roomId));
+        if (roomIds.size < upgradeHistory.length) {
+            // The last room added to the list introduced a previous roomId
+            // To avoid recursion, return the last rooms - 1
+            return upgradeHistory.slice(0, upgradeHistory.length - 1);
+        }
+
+        // Set the current room to the reference room so we know where we're at
+        currentRoom = refRoom;
+        tombstoneEvent = currentRoom.currentState.getStateEvents("m.room.tombstone", "");
+    }
+
+    return upgradeHistory;
 };
 
 /**
  * @param {string} roomId
  * @param {string} userId
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.invite = function(roomId, userId, callback) {
-    return _membershipChange(this, roomId, userId, "invite", undefined,
-        callback);
+MatrixClient.prototype.invite = function (roomId, userId, callback) {
+    return _membershipChange(this, roomId, userId, "invite", undefined, callback);
 };
 
 /**
  * Invite a user to a room based on their email address.
  * @param {string} roomId The room to invite the user to.
  * @param {string} email The email address to invite.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.inviteByEmail = function(roomId, email, callback) {
-    return this.inviteByThreePid(
-        roomId, "email", email, callback
-    );
+MatrixClient.prototype.inviteByEmail = function (roomId, email, callback) {
+    return this.inviteByThreePid(roomId, "email", email, callback);
 };
 
 /**
  * Invite a user to a room based on a third-party identifier.
  * @param {string} roomId The room to invite the user to.
  * @param {string} medium The medium to invite the user e.g. "email".
  * @param {string} address The address for the specified medium.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.inviteByThreePid = function(roomId, medium, address, callback) {
-    var path = utils.encodeUri(
-        "/rooms/$roomId/invite",
-        { $roomId: roomId }
-    );
-
-    var identityServerUrl = this.getIdentityServerUrl();
+MatrixClient.prototype.inviteByThreePid = async function (roomId, medium, address, callback) {
+    const path = utils.encodeUri("/rooms/$roomId/invite", { $roomId: roomId });
+
+    const identityServerUrl = this.getIdentityServerUrl(true);
     if (!identityServerUrl) {
-        return q.reject(new MatrixError({
+        return _bluebird2.default.reject(new MatrixError({
             error: "No supplied identity server URL",
             errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM"
         }));
     }
-    if (identityServerUrl.indexOf("http://") === 0 ||
-            identityServerUrl.indexOf("https://") === 0) {
-        // this request must not have the protocol part because reasons
-        identityServerUrl = identityServerUrl.split("://")[1];
-    }
-
-    return this._http.authedRequest(callback, "POST", path, undefined, {
+    const params = {
         id_server: identityServerUrl,
         medium: medium,
         address: address
-    });
+    };
+
+    if (this.identityServer && this.identityServer.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) {
+        const identityAccessToken = await this.identityServer.getAccessToken();
+        if (identityAccessToken) {
+            params.id_access_token = identityAccessToken;
+        }
+    }
+
+    return this._http.authedRequest(callback, "POST", path, undefined, params);
 };
 
 /**
  * @param {string} roomId
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.leave = function(roomId, callback) {
-    return _membershipChange(this, roomId, undefined, "leave", undefined,
-        callback);
+MatrixClient.prototype.leave = function (roomId, callback) {
+    return _membershipChange(this, roomId, undefined, "leave", undefined, callback);
+};
+
+/**
+ * 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 {string} roomId The room ID to start leaving at
+ * @param {boolean} includeFuture If true, the whole chain (past and future) of
+ * upgraded rooms will be left.
+ * @return {module:client.Promise} Resolves when completed with an object keyed
+ * by room ID and value of the error encountered when leaving or null.
+ */
+MatrixClient.prototype.leaveRoomChain = function (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 = {}; // {roomId: Error}
+    const promises = [];
+
+    const doLeave = roomId => {
+        return this.leave(roomId).then(() => {
+            populationResults[roomId] = null;
+        }).catch(err => {
+            populationResults[roomId] = err;
+            return null; // suppress error
+        });
+    };
+
+    for (const room of eligibleToLeave) {
+        promises.push(doLeave(room.roomId));
+    }
+
+    return _bluebird2.default.all(promises).then(() => populationResults);
 };
 
 /**
  * @param {string} roomId
  * @param {string} userId
  * @param {string} reason Optional.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.ban = function(roomId, userId, reason, callback) {
-    return _membershipChange(this, roomId, userId, "ban", reason,
-        callback);
+MatrixClient.prototype.ban = function (roomId, userId, reason, callback) {
+    return _membershipChange(this, roomId, userId, "ban", reason, callback);
 };
 
 /**
  * @param {string} roomId
  * @param {boolean} deleteRoom True to delete the room from the store on success.
  * Default: true.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.forget = function(roomId, deleteRoom, callback) {
+MatrixClient.prototype.forget = function (roomId, deleteRoom, callback) {
     if (deleteRoom === undefined) {
         deleteRoom = true;
     }
-    var promise = _membershipChange(this, roomId, undefined, "forget", undefined,
-        callback);
+    const promise = _membershipChange(this, roomId, undefined, "forget", undefined, callback);
     if (!deleteRoom) {
         return promise;
     }
-    var self = this;
-    return promise.then(function(response) {
+    const self = this;
+    return promise.then(function (response) {
         self.store.removeRoom(roomId);
         self.emit("deleteRoom", roomId);
         return response;
     });
 };
 
 /**
  * @param {string} roomId
  * @param {string} userId
  * @param {module:client.callback} callback Optional.
- * @return {module:client.Promise} Resolves: TODO
+ * @return {module:client.Promise} Resolves: Object (currently empty)
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.unban = function(roomId, userId, callback) {
-    // unbanning = set their state to leave
-    return _setMembershipState(
-        this, roomId, userId, "leave", undefined, callback
-    );
+MatrixClient.prototype.unban = function (roomId, userId, callback) {
+    // unbanning != set their state to leave: this used to be
+    // the case, but was then changed so that leaving was always
+    // a revoking of priviledge, 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(callback, "POST", path, undefined, data);
 };
 
 /**
  * @param {string} roomId
  * @param {string} userId
  * @param {string} reason Optional.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.kick = function(roomId, userId, reason, callback) {
-    return _setMembershipState(
-        this, roomId, userId, "leave", reason, callback
-    );
+MatrixClient.prototype.kick = function (roomId, userId, reason, callback) {
+    return _setMembershipState(this, roomId, userId, "leave", reason, callback);
 };
 
 /**
  * This is an internal method.
  * @param {MatrixClient} client
  * @param {string} roomId
  * @param {string} userId
  * @param {string} membershipValue
  * @param {string} reason
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-function _setMembershipState(client, roomId, userId, membershipValue, reason,
-                             callback) {
-    if (utils.isFunction(reason)) { callback = reason; reason = undefined; }
-
-    var path = utils.encodeUri(
-        "/rooms/$roomId/state/m.room.member/$userId",
-        { $roomId: roomId, $userId: userId}
-    );
+function _setMembershipState(client, roomId, userId, membershipValue, reason, callback) {
+    if (utils.isFunction(reason)) {
+        callback = reason;reason = undefined;
+    }
+
+    const path = utils.encodeUri("/rooms/$roomId/state/m.room.member/$userId", { $roomId: roomId, $userId: userId });
 
     return client._http.authedRequest(callback, "PUT", path, undefined, {
         membership: membershipValue,
         reason: reason
     });
 }
 
 /**
@@ -1348,133 +2540,188 @@ function _setMembershipState(client, roo
  * @param {string} userId
  * @param {string} membership
  * @param {string} reason
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
 function _membershipChange(client, roomId, userId, membership, reason, callback) {
-    if (utils.isFunction(reason)) { callback = reason; reason = undefined; }
-
-    var path = utils.encodeUri("/rooms/$room_id/$membership", {
+    if (utils.isFunction(reason)) {
+        callback = reason;reason = undefined;
+    }
+
+    const path = utils.encodeUri("/rooms/$room_id/$membership", {
         $room_id: roomId,
         $membership: membership
     });
-    return client._http.authedRequest(
-        callback, "POST", path, undefined, {
-            user_id: userId,  // may be undefined e.g. on leave
-            reason: reason
-        }
-    );
+    return client._http.authedRequest(callback, "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 {MatrixEvent} event The event to get push actions for.
  * @return {module:pushprocessor~PushAction} A dict of actions to perform.
  */
-MatrixClient.prototype.getPushActionsForEvent = function(event) {
+MatrixClient.prototype.getPushActionsForEvent = function (event) {
     if (!event.getPushActions()) {
-        var pushProcessor = new PushProcessor(this);
-        event.setPushActions(pushProcessor.actionsForEvent(event));
+        event.setPushActions(this._pushProcessor.actionsForEvent(event));
     }
     return event.getPushActions();
 };
 
 // Profile operations
 // ==================
 
 /**
  * @param {string} info The kind of info to set (e.g. 'avatar_url')
  * @param {Object} data The JSON object to set.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.setProfileInfo = function(info, data, callback) {
-    var path = utils.encodeUri("/profile/$userId/$info", {
+MatrixClient.prototype.setProfileInfo = function (info, data, callback) {
+    const path = utils.encodeUri("/profile/$userId/$info", {
         $userId: this.credentials.userId,
         $info: info
     });
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, data
-    );
+    return this._http.authedRequest(callback, "PUT", path, undefined, data);
 };
 
 /**
  * @param {string} name
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.setDisplayName = function(name, callback) {
-    return this.setProfileInfo(
-        "displayname", { displayname: name }, callback
-    );
+MatrixClient.prototype.setDisplayName = function (name, callback) {
+    return this.setProfileInfo("displayname", { displayname: name }, callback);
 };
 
 /**
  * @param {string} url
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.setAvatarUrl = function(url, callback) {
-    return this.setProfileInfo(
-        "avatar_url", { avatar_url: url }, callback
-    );
+MatrixClient.prototype.setAvatarUrl = function (url, callback) {
+    return this.setProfileInfo("avatar_url", { avatar_url: url }, callback);
 };
 
 /**
  * Turn an MXC URL into an HTTP one. <strong>This method is experimental and
  * may change.</strong>
  * @param {string} mxcUrl The MXC URL
  * @param {Number} width The desired width of the thumbnail.
  * @param {Number} height The desired height of the thumbnail.
  * @param {string} resizeMethod The thumbnail resize method to use, either
  * "crop" or "scale".
  * @param {Boolean} 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.
  * @return {?string} the avatar URL or null.
  */
-MatrixClient.prototype.mxcUrlToHttp =
-        function(mxcUrl, width, height, resizeMethod, allowDirectLinks) {
-    return contentRepo.getHttpUriForMxc(
-        this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks
-    );
+MatrixClient.prototype.mxcUrlToHttp = function (mxcUrl, width, height, resizeMethod, allowDirectLinks) {
+    return contentRepo.getHttpUriForMxc(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks);
+};
+
+/**
+ * Sets a new status message for the user. The message may be null/falsey
+ * to clear the message.
+ * @param {string} newMessage The new message to set.
+ * @return {module:client.Promise} Resolves: to nothing
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+MatrixClient.prototype._unstable_setStatusMessage = function (newMessage) {
+    const type = "im.vector.user_status";
+    return _bluebird2.default.all(this.getRooms().map(room => {
+        const isJoined = room.getMyMembership() === "join";
+        const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2;
+        if (!isJoined || !looksLikeDm) {
+            return _bluebird2.default.resolve();
+        }
+        // Check power level separately as it's a bit more expensive.
+        const maySend = room.currentState.mayClientSendStateEvent(type, this);
+        if (!maySend) {
+            return _bluebird2.default.resolve();
+        }
+        return this.sendStateEvent(room.roomId, type, {
+            status: newMessage
+        }, this.getUserId());
+    }));
 };
 
 /**
  * @param {Object} opts Options to apply
  * @param {string} opts.presence One of "online", "offline" or "unavailable"
  * @param {string} opts.status_msg The status message to attach.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  * @throws If 'presence' isn't a valid presence enum value.
  */
-MatrixClient.prototype.setPresence = function(opts, callback) {
-    var path = utils.encodeUri("/presence/$userId/status", {
+MatrixClient.prototype.setPresence = function (opts, callback) {
+    const path = utils.encodeUri("/presence/$userId/status", {
         $userId: this.credentials.userId
     });
 
     if (typeof opts === "string") {
-      opts = { presence: opts };
+        opts = { presence: opts };
     }
 
-    var validStates = ["offline", "online", "unavailable"];
+    const validStates = ["offline", "online", "unavailable"];
     if (validStates.indexOf(opts.presence) == -1) {
         throw new Error("Bad presence value: " + opts.presence);
     }
-    return this._http.authedRequest(
-        callback, "PUT", path, undefined, opts
-    );
+    return this._http.authedRequest(callback, "PUT", path, undefined, opts);
+};
+
+function _presenceList(callback, client, opts, method) {
+    const path = utils.encodeUri("/presence/list/$userId", {
+        $userId: client.credentials.userId
+    });
+    return client._http.authedRequest(callback, method, path, undefined, opts);
+}
+
+/**
+* Retrieve current user presence list.
+* @param {module:client.callback} callback Optional.
+* @return {module:client.Promise} Resolves: TODO
+* @return {module:http-api.MatrixError} Rejects: with an error response.
+*/
+MatrixClient.prototype.getPresenceList = function (callback) {
+    return _presenceList(callback, this, undefined, "GET");
+};
+
+/**
+* Add users to the current user presence list.
+* @param {module:client.callback} callback Optional.
+* @param {string[]} userIds
+* @return {module:client.Promise} Resolves: TODO
+* @return {module:http-api.MatrixError} Rejects: with an error response.
+*/
+MatrixClient.prototype.inviteToPresenceList = function (callback, userIds) {
+    const opts = { "invite": userIds };
+    return _presenceList(callback, this, opts, "POST");
+};
+
+/**
+* Drop users from the current user presence list.
+* @param {module:client.callback} callback Optional.
+* @param {string[]} userIds
+* @return {module:client.Promise} Resolves: TODO
+* @return {module:http-api.MatrixError} Rejects: with an error response.
+**/
+MatrixClient.prototype.dropFromPresenceList = function (callback, userIds) {
+    const opts = { "drop": userIds };
+    return _presenceList(callback, this, opts, "POST");
 };
 
 /**
  * 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
@@ -1484,380 +2731,329 @@ MatrixClient.prototype.setPresence = fun
  * @param {Integer} limit Optional. The maximum number of previous events to
  * pull in. Default: 30.
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: Room. If you are at the beginning
  * of the timeline, <code>Room.oldState.paginationToken</code> will be
  * <code>null</code>.
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.scrollback = function(room, limit, callback) {
-    if (utils.isFunction(limit)) { callback = limit; limit = undefined; }
+MatrixClient.prototype.scrollback = function (room, limit, callback) {
+    if (utils.isFunction(limit)) {
+        callback = limit;limit = undefined;
+    }
     limit = limit || 30;
-    var timeToWaitMs = 0;
-
-    var info = this._ongoingScrollbacks[room.roomId] || {};
+    let timeToWaitMs = 0;
+
+    let info = this._ongoingScrollbacks[room.roomId] || {};
     if (info.promise) {
         return info.promise;
-    }
-    else if (info.errorTs) {
-        var timeWaitedMs = Date.now() - info.errorTs;
+    } else if (info.errorTs) {
+        const timeWaitedMs = Date.now() - info.errorTs;
         timeToWaitMs = Math.max(SCROLLBACK_DELAY_MS - timeWaitedMs, 0);
     }
 
     if (room.oldState.paginationToken === null) {
-        return q(room); // already at the start.
+        return _bluebird2.default.resolve(room); // already at the start.
     }
     // attempt to grab more events from the store first
-    var numAdded = this.store.scrollback(room, limit).length;
+    const numAdded = this.store.scrollback(room, limit).length;
     if (numAdded === limit) {
         // store contained everything we needed.
-        return q(room);
+        return _bluebird2.default.resolve(room);
     }
     // reduce the required number of events appropriately
     limit = limit - numAdded;
 
-    var path = utils.encodeUri(
-        "/rooms/$roomId/messages", {$roomId: room.roomId}
-    );
-    var params = {
-        from: room.oldState.paginationToken,
-        limit: limit,
-        dir: 'b'
-    };
-    var defer = q.defer();
+    const defer = _bluebird2.default.defer();
     info = {
         promise: defer.promise,
         errorTs: null
     };
-    var self = this;
+    const self = this;
     // wait for a time before doing this request
     // (which may be 0 in order not to special case the code paths)
-    q.delay(timeToWaitMs).then(function() {
-        return self._http.authedRequest(callback, "GET", path, params);
-    }).done(function(res) {
-        var matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self));
+    _bluebird2.default.delay(timeToWaitMs).then(function () {
+        return self._createMessagesRequest(room.roomId, room.oldState.paginationToken, limit, 'b');
+    }).done(function (res) {
+        const matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self));
+        if (res.state) {
+            const stateEvents = utils.map(res.state, _PojoToMatrixEventMapper(self));
+            room.currentState.setUnknownStateEvents(stateEvents);
+        }
         room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline());
         room.oldState.paginationToken = res.end;
         if (res.chunk.length === 0) {
             room.oldState.paginationToken = null;
         }
         self.store.storeEvents(room, matrixEvents, res.end, true);
         self._ongoingScrollbacks[room.roomId] = null;
         _resolve(callback, defer, room);
-    }, function(err) {
+    }, function (err) {
         self._ongoingScrollbacks[room.roomId] = {
             errorTs: Date.now()
         };
         _reject(callback, defer, err);
     });
     this._ongoingScrollbacks[room.roomId] = info;
     return defer.promise;
 };
 
 /**
- * Take an EventContext, and back/forward-fill results.
- *
- * @param {module:models/event-context.EventContext} eventContext  context
- *    object to be updated
- * @param {Object}  opts
- * @param {boolean} opts.backwards  true to fill backwards, false to go forwards
- * @param {boolean} opts.limit      number of events to request
- *
- * @return {module:client.Promise} Resolves: updated EventContext object
- * @return {Error} Rejects: with an error response.
- */
-MatrixClient.prototype.paginateEventContext = function(eventContext, opts) {
-    // TODO: we should implement a backoff (as per scrollback()) to deal more
-    // nicely with HTTP errors.
-    opts = opts || {};
-    var backwards = opts.backwards || false;
-
-    var token = eventContext.getPaginateToken(backwards);
-    if (!token) {
-        // no more results.
-        return q.reject(new Error("No paginate token"));
-    }
-
-    var dir = backwards ? 'b' : 'f';
-    var pendingRequest = eventContext._paginateRequests[dir];
-
-    if (pendingRequest) {
-        // already a request in progress - return the existing promise
-        return pendingRequest;
-    }
-
-    var path = utils.encodeUri(
-        "/rooms/$roomId/messages", {$roomId: eventContext.getEvent().getRoomId()}
-    );
-    var params = {
-        from: token,
-        limit: ('limit' in opts) ? opts.limit : 30,
-        dir: dir
-    };
-
-    var self = this;
-    var promise =
-        self._http.authedRequest(undefined, "GET", path, params
-    ).then(function(res) {
-        var token = res.end;
-        if (res.chunk.length === 0) {
-            token = null;
-        } else {
-            var matrixEvents = utils.map(res.chunk, self.getEventMapper());
-            if (backwards) {
-                // eventContext expects the events in timeline order, but
-                // back-pagination returns them in reverse order.
-                matrixEvents.reverse();
-            }
-            eventContext.addEvents(matrixEvents, backwards);
-        }
-        eventContext.setPaginateToken(token, backwards);
-        return eventContext;
-    }).finally(function() {
-        eventContext._paginateRequests[dir] = null;
-    });
-    eventContext._paginateRequests[dir] = promise;
-
-    return promise;
-};
-
-/**
  * 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.
  *
  * @param {EventTimelineSet} timelineSet  The timelineSet to look for the event in
  * @param {string} eventId  The ID of the event to look for
  *
  * @return {module:client.Promise} Resolves:
  *    {@link module:models/event-timeline~EventTimeline} including the given
  *    event
  */
-MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
+MatrixClient.prototype.getEventTimeline = function (timelineSet, eventId) {
     // 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.");
+        throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable" + " it.");
     }
 
     if (timelineSet.getTimelineForEvent(eventId)) {
-        return q(timelineSet.getTimelineForEvent(eventId));
+        return _bluebird2.default.resolve(timelineSet.getTimelineForEvent(eventId));
     }
 
-    var path = utils.encodeUri(
-        "/rooms/$roomId/context/$eventId", {
-            $roomId: timelineSet.room.roomId,
-            $eventId: eventId,
-        }
-    );
+    const path = utils.encodeUri("/rooms/$roomId/context/$eventId", {
+        $roomId: timelineSet.room.roomId,
+        $eventId: eventId
+    });
+
+    let params = undefined;
+    if (this._clientOpts.lazyLoadMembers) {
+        params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) };
+    }
 
     // TODO: we should implement a backoff (as per scrollback()) to deal more
     // nicely with HTTP errors.
-    var self = this;
-    var promise =
-        self._http.authedRequest(undefined, "GET", path
-    ).then(function(res) {
+    const self = this;
+    const promise = self._http.authedRequest(undefined, "GET", path, params).then(function (res) {
         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);
         }
 
         // 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();
-        var events = res.events_after
-            .concat([res.event])
-            .concat(res.events_before);
-        var matrixEvents = utils.map(events, self.getEventMapper());
-
-        var timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId());
+        const events = res.events_after.concat([res.event]).concat(res.events_before);
+        const matrixEvents = utils.map(events, self.getEventMapper());
+
+        let timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId());
         if (!timeline) {
             timeline = timelineSet.addTimeline();
-            timeline.initialiseState(utils.map(res.state,
-                                               self.getEventMapper()));
+            timeline.initialiseState(utils.map(res.state, self.getEventMapper()));
             timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
+        } else {
+            const stateEvents = utils.map(res.state, self.getEventMapper());
+            timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents);
         }
         timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start);
 
         // 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.
-        var tl = timelineSet.getTimelineForEvent(eventId) || timeline;
+        const tl = timelineSet.getTimelineForEvent(eventId) || timeline;
         return tl;
     });
     return promise;
 };
 
+/**
+ * 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 {string} roomId
+ * @param {string} fromToken
+ * @param {number} limit the maximum amount of events the retrieve
+ * @param {string} dir 'f' or 'b'
+ * @param {Filter} timelineFilter the timeline filter to pass
+ * @return {Promise}
+ */
+MatrixClient.prototype._createMessagesRequest = function (roomId, fromToken, limit, dir, timelineFilter = undefined) {
+    const path = utils.encodeUri("/rooms/$roomId/messages", { $roomId: roomId });
+    if (limit === undefined) {
+        limit = 30;
+    }
+    const params = {
+        from: fromToken,
+        limit: limit,
+        dir: dir
+    };
+
+    let filter = null;
+    if (this._clientOpts.lazyLoadMembers) {
+        // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
+        // so the timelineFilter doesn't get written into it below
+        filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER);
+    }
+    if (timelineFilter) {
+        // 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.getRoomTimelineFilterComponent());
+    }
+    if (filter) {
+        params.filter = JSON.stringify(filter);
+    }
+    return this._http.authedRequest(undefined, "GET", path, params);
+};
 
 /**
  * Take an EventTimeline, and back/forward-fill results.
  *
  * @param {module:models/event-timeline~EventTimeline} eventTimeline timeline
  *    object to be updated
  * @param {Object}   [opts]
  * @param {bool}     [opts.backwards = false]  true to fill backwards,
  *    false to go forwards
  * @param {number}   [opts.limit = 30]         number of events to request
  *
  * @return {module:client.Promise} Resolves to a boolean: false if there are no
  *    events and we reached either end of the timeline; else true.
  */
-MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
-    var isNotifTimeline = (eventTimeline.getTimelineSet() === this._notifTimelineSet);
+MatrixClient.prototype.paginateEventTimeline = function (eventTimeline, opts) {
+    const isNotifTimeline = eventTimeline.getTimelineSet() === this._notifTimelineSet;
 
     // TODO: we should implement a backoff (as per scrollback()) to deal more
     // nicely with HTTP errors.
     opts = opts || {};
-    var backwards = opts.backwards || false;
+    const backwards = opts.backwards || false;
 
     if (isNotifTimeline) {
         if (!backwards) {
             throw new Error("paginateNotifTimeline can only paginate backwards");
         }
     }
 
-    var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
-
-    var token = eventTimeline.getPaginationToken(dir);
+    const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
+
+    const token = eventTimeline.getPaginationToken(dir);
     if (!token) {
         // no token - no results.
-        return q(false);
+        return _bluebird2.default.resolve(false);
     }
 
-    var pendingRequest = eventTimeline._paginationRequests[dir];
+    const pendingRequest = eventTimeline._paginationRequests[dir];
 
     if (pendingRequest) {
         // already a request in progress - return the existing promise
         return pendingRequest;
     }
 
-    var path, params, promise;
-    var self = this;
+    let path, params, promise;
+    const self = this;
 
     if (isNotifTimeline) {
         path = "/notifications";
         params = {
-            limit: ('limit' in opts) ? opts.limit : 30,
-            only: 'highlight',
+            limit: 'limit' in opts ? opts.limit : 30,
+            only: 'highlight'
         };
 
         if (token && token !== "end") {
             params.from = token;
         }
 
-        promise =
-            this._http.authedRequestWithPrefix(undefined, "GET", path, params,
-                undefined, httpApi.PREFIX_UNSTABLE
-        ).then(function(res) {
-            var token = res.next_token;
-            var matrixEvents = [];
-
-            for (var i = 0; i < res.notifications.length; i++) {
-                var notification = res.notifications[i];
-                var event = self.getEventMapper()(notification.event);
-                event.setPushActions(
-                    PushProcessor.actionListToActionsObject(notification.actions)
-                );
+        promise = this._http.authedRequest(undefined, "GET", path, params, undefined).then(function (res) {
+            const token = res.next_token;
+            const matrixEvents = [];
+
+            for (let i = 0; i < res.notifications.length; i++) {
+                const notification = res.notifications[i];
+                const event = self.getEventMapper()(notification.event);
+                event.setPushActions(PushProcessor.actionListToActionsObject(notification.actions));
                 event.event.room_id = notification.room_id; // XXX: gutwrenching
                 matrixEvents[i] = event;
             }
 
-            eventTimeline.getTimelineSet()
-                .addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
+            eventTimeline.getTimelineSet().addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
 
             // 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 res.next_token ? true : false;
-        }).finally(function() {
+        }).finally(function () {
             eventTimeline._paginationRequests[dir] = null;
         });
         eventTimeline._paginationRequests[dir] = promise;
-    }
-    else {
-        var room = this.getRoom(eventTimeline.getRoomId());
+    } else {
+        const room = this.getRoom(eventTimeline.getRoomId());
         if (!room) {
             throw new Error("Unknown room " + eventTimeline.getRoomId());
         }
 
-        path = utils.encodeUri(
-            "/rooms/$roomId/messages", {$roomId: eventTimeline.getRoomId()}
-        );
-        params = {
-            from: token,
-            limit: ('limit' in opts) ? opts.limit : 30,
-            dir: dir
-        };
-
-        var filter = eventTimeline.getFilter();
-        if (filter) {
-            // XXX: it's horrific that /messages' filter parameter doesn't match
-            // /sync's one - see https://matrix.org/jira/browse/SPEC-451
-            params.filter = JSON.stringify(filter.getRoomTimelineFilterComponent());
-        }
-
-        promise =
-            this._http.authedRequest(undefined, "GET", path, params
-        ).then(function(res) {
-            var token = res.end;
-            var matrixEvents = utils.map(res.chunk, self.getEventMapper());
-            eventTimeline.getTimelineSet()
-                .addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
+        promise = this._createMessagesRequest(eventTimeline.getRoomId(), token, opts.limit, dir, eventTimeline.getFilter());
+        promise.then(function (res) {
+            if (res.state) {
+                const roomState = eventTimeline.getState(dir);
+                const stateEvents = utils.map(res.state, self.getEventMapper());
+                roomState.setUnknownStateEvents(stateEvents);
+            }
+            const token = res.end;
+            const matrixEvents = utils.map(res.chunk, self.getEventMapper());
+            eventTimeline.getTimelineSet().addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
 
             // 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(function() {
+        }).finally(function () {
             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.
  */
-MatrixClient.prototype.resetNotifTimelineSet = function() {
+MatrixClient.prototype.resetNotifTimelineSet = function () {
     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', true);
+    this._notifTimelineSet.resetLiveTimeline('end', null);
 
     // 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,
@@ -1868,28 +3064,28 @@ MatrixClient.prototype.resetNotifTimelin
 
 /**
  * Peek into a room and receive updates about the room. This only works if the
  * history visibility for the room is world_readable.
  * @param {String} roomId The room to attempt to peek into.
  * @return {module:client.Promise} Resolves: Room object
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.peekInRoom = function(roomId) {
+MatrixClient.prototype.peekInRoom = function (roomId) {
     if (this._peekSync) {
         this._peekSync.stopPeeking();
     }
     this._peekSync = new SyncApi(this, this._clientOpts);
     return this._peekSync.peek(roomId);
 };
 
 /**
  * Stop any ongoing room peeking.
  */
-MatrixClient.prototype.stopPeeking = function() {
+MatrixClient.prototype.stopPeeking = function () {
     if (this._peekSync) {
         this._peekSync.stopPeeking();
         this._peekSync = null;
     }
 };
 
 /**
  * Set r/w flags for guest access in a room.
@@ -1899,88 +3095,129 @@ MatrixClient.prototype.stopPeeking = fun
  * implicitly gives guests write access. If false or not given, guests are
  * explicitly forbidden from joining the room.
  * @param {boolean} opts.allowRead True to set history visibility to
  * be world_readable. This gives guests read access *from this point forward*.
  * If false or not given, history visibility is not modified.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.setGuestAccess = function(roomId, opts) {
-    var writePromise = this.sendStateEvent(roomId, "m.room.guest_access", {
+MatrixClient.prototype.setGuestAccess = function (roomId, opts) {
+    const writePromise = this.sendStateEvent(roomId, "m.room.guest_access", {
         guest_access: opts.allowJoin ? "can_join" : "forbidden"
     });
 
-    var readPromise = q();
+    let readPromise = _bluebird2.default.resolve();
     if (opts.allowRead) {
         readPromise = this.sendStateEvent(roomId, "m.room.history_visibility", {
             history_visibility: "world_readable"
         });
     }
 
-    return q.all(readPromise, writePromise);
+    return _bluebird2.default.all([readPromise, writePromise]);
 };
 
 // Registration/Login operations
 // =============================
 
 /**
  * Requests an email verification token for the purposes of registration.
- * This API proxies the Identity Server /validate/email/requestToken API,
- * adding registration-specific behaviour. Specifically, if an account with
- * the given email address already exists, 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 Home Server).
- *
- * requestEmailToken calls the equivalent API directly on the ID server,
- * therefore bypassing the registration-specific logic.
+ * 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 {string} email As requestEmailToken
  * @param {string} clientSecret As requestEmailToken
  * @param {number} sendAttempt As requestEmailToken
  * @param {string} nextLink As requestEmailToken
- * @param {module:client.callback} callback Optional. As requestEmailToken
  * @return {module:client.Promise} Resolves: As requestEmailToken
  */
-MatrixClient.prototype.requestRegisterEmailToken = function(email, clientSecret,
-                                                    sendAttempt, nextLink, callback) {
-    return this._requestTokenFromEndpoint(
-        "/register/email/requestToken",
-        email, clientSecret, sendAttempt, nextLink, callback
-    );
+MatrixClient.prototype.requestRegisterEmailToken = function (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 {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in which
+ *    phoneNumber should be parsed relative to.
+ * @param {string} phoneNumber The phone number, in national or international format
+ * @param {string} clientSecret As requestEmailToken
+ * @param {number} sendAttempt As requestEmailToken
+ * @param {string} nextLink As requestEmailToken
+ * @return {module:client.Promise} Resolves: As requestEmailToken
+ */
+MatrixClient.prototype.requestRegisterMsisdnToken = function (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 proxies the Identity Server /validate/email/requestToken API,
- * adding specific behaviour for the addition of email addresses to an
- * account. Specifically, if an account with
- * the given email address already exists, 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 Home Server).
- *
- * requestEmailToken calls the equivalent API directly on the ID server,
- * therefore bypassing the email addition specific logic.
+ * 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 Home Server).
  *
  * @param {string} email As requestEmailToken
  * @param {string} clientSecret As requestEmailToken
  * @param {number} sendAttempt As requestEmailToken
  * @param {string} nextLink As requestEmailToken
- * @param {module:client.callback} callback Optional. As requestEmailToken
  * @return {module:client.Promise} Resolves: As requestEmailToken
  */
-MatrixClient.prototype.requestAdd3pidEmailToken = function(email, clientSecret,
-                                                    sendAttempt, nextLink, callback) {
-    return this._requestTokenFromEndpoint(
-        "/account/3pid/email/requestToken",
-        email, clientSecret, sendAttempt, nextLink, callback
-    );
+MatrixClient.prototype.requestAdd3pidEmailToken = function (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 {string} phoneCountry As requestRegisterMsisdnToken
+ * @param {string} phoneNumber As requestRegisterMsisdnToken
+ * @param {string} clientSecret As requestEmailToken
+ * @param {number} sendAttempt As requestEmailToken
+ * @param {string} nextLink As requestEmailToken
+ * @return {module:client.Promise} Resolves: As requestEmailToken
+ */
+MatrixClient.prototype.requestAdd3pidMsisdnToken = function (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
@@ -1992,156 +3229,171 @@ MatrixClient.prototype.requestAdd3pidEma
  *
  * @param {string} email As requestEmailToken
  * @param {string} clientSecret As requestEmailToken
  * @param {number} sendAttempt As requestEmailToken
  * @param {string} nextLink As requestEmailToken
  * @param {module:client.callback} callback Optional. As requestEmailToken
  * @return {module:client.Promise} Resolves: As requestEmailToken
  */
-MatrixClient.prototype.requestPasswordEmailToken = function(email, clientSecret,
-                                                    sendAttempt, nextLink, callback) {
-    return this._requestTokenFromEndpoint(
-        "/account/password/email/requestToken",
-        email, clientSecret, sendAttempt, nextLink, callback
-    );
+MatrixClient.prototype.requestPasswordEmailToken = function (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 {string} phoneCountry As requestRegisterMsisdnToken
+ * @param {string} phoneNumber As requestRegisterMsisdnToken
+ * @param {string} clientSecret As requestEmailToken
+ * @param {number} sendAttempt As requestEmailToken
+ * @param {string} nextLink As requestEmailToken
+ * @return {module:client.Promise} Resolves: As requestEmailToken
+ */
+MatrixClient.prototype.requestPasswordMsisdnToken = function (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 {string} endpoint The endpoint to send the request to
- * @param {string} email As requestEmailToken
- * @param {string} clientSecret As requestEmailToken
- * @param {number} sendAttempt As requestEmailToken
- * @param {string} nextLink As requestEmailToken
- * @param {module:client.callback} callback Optional. As requestEmailToken
+ * @param {object} params Parameters for the POST request
  * @return {module:client.Promise} Resolves: As requestEmailToken
  */
-MatrixClient.prototype._requestTokenFromEndpoint = function(endpoint,
-                                                    email, clientSecret,
-                                                    sendAttempt, nextLink, callback) {
-    var id_server_url = url.parse(this.idBaseUrl);
-    if (id_server_url.host === null) {
-        throw new Error("Invalid ID server URL: " + this.idBaseUrl);
+MatrixClient.prototype._requestTokenFromEndpoint = async function (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) {
+        const idServerUrl = url.parse(this.idBaseUrl);
+        if (!idServerUrl.host) {
+            throw new Error("Invalid ID server URL: " + this.idBaseUrl);
+        }
+        postParams.id_server = idServerUrl.host;
+
+        if (this.identityServer && this.identityServer.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) {
+            const identityAccessToken = await this.identityServer.getAccessToken();
+            if (identityAccessToken) {
+                postParams.id_access_token = identityAccessToken;
+            }
+        }
     }
 
-    var params = {
-        client_secret: clientSecret,
-        email: email,
-        send_attempt: sendAttempt,
-        next_link: nextLink,
-        id_server: id_server_url.host,
-    };
-    return this._http.request(
-        callback, "POST", endpoint, undefined,
-        params
-    );
+    return this._http.request(undefined, "POST", endpoint, undefined, postParams);
 };
 
-
 // Push operations
 // ===============
 
 /**
  * Get the room-kind push rule associated with a room.
  * @param {string} scope "global" or device-specific.
  * @param {string} roomId the id of the room.
  * @return {object} the rule or undefined.
  */
-MatrixClient.prototype.getRoomPushRule = function(scope, roomId) {
+MatrixClient.prototype.getRoomPushRule = function (scope, roomId) {
     // There can be only room-kind push rule per room
     // and its id is the room id.
     if (this.pushRules) {
-        for (var i = 0; i < this.pushRules[scope].room.length; i++) {
-            var rule = this.pushRules[scope].room[i];
+        for (let i = 0; i < this.pushRules[scope].room.length; i++) {
+            const rule = this.pushRules[scope].room[i];
             if (rule.rule_id === roomId) {
                 return rule;
             }
         }
-    }
-    else {
-        throw new Error(
-            "SyncApi.sync() must be done before accessing to push rules."
-        );
+    } 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 {string} scope "global" or device-specific.
  * @param {string} roomId the id of the room.
  * @param {string} mute the mute state.
  * @return {module:client.Promise} Resolves: result object
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
-    var self = this;
-    var deferred, hasDontNotifyRule;
+MatrixClient.prototype.setRoomMutePushRule = function (scope, roomId, mute) {
+    const self = this;
+    let deferred, hasDontNotifyRule;
 
     // Get the existing room-kind push rule if any
-    var roomPushRule = this.getRoomPushRule(scope, roomId);
+    const roomPushRule = this.getRoomPushRule(scope, roomId);
     if (roomPushRule) {
         if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
             hasDontNotifyRule = true;
         }
     }
 
     if (!mute) {
         // Remove the rule only if it is a muting rule
         if (hasDontNotifyRule) {
             deferred = this.deletePushRule(scope, "room", roomPushRule.rule_id);
         }
-    }
-    else {
+    } else {
         if (!roomPushRule) {
             deferred = this.addPushRule(scope, "room", roomId, {
                 actions: ["dont_notify"]
             });
-        }
-        else if (!hasDontNotifyRule) {
+        } else if (!hasDontNotifyRule) {
             // Remove the existing one before setting the mute push rule
             // This is a workaround to SYN-590 (Push rule update fails)
-            deferred = q.defer();
-            this.deletePushRule(scope, "room", roomPushRule.rule_id)
-            .done(function() {
+            deferred = _bluebird2.default.defer();
+            this.deletePushRule(scope, "room", roomPushRule.rule_id).done(function () {
                 self.addPushRule(scope, "room", roomId, {
                     actions: ["dont_notify"]
-                }).done(function() {
+                }).done(function () {
                     deferred.resolve();
-                }, function(err) {
+                }, function (err) {
                     deferred.reject(err);
                 });
-            }, function(err) {
+            }, function (err) {
                 deferred.reject(err);
             });
 
             deferred = deferred.promise;
         }
     }
 
     if (deferred) {
         // Update this.pushRules when the operation completes
-        var ruleRefreshDeferred = q.defer();
-        deferred.done(function() {
-            self.getPushRules().done(function(result) {
+        const ruleRefreshDeferred = _bluebird2.default.defer();
+        deferred.done(function () {
+            self.getPushRules().done(function (result) {
                 self.pushRules = result;
                 ruleRefreshDeferred.resolve();
-            }, function(err) {
+            }, function (err) {
                 ruleRefreshDeferred.reject(err);
             });
-        }, function(err) {
+        }, function (err) {
             // Update it even if the previous operation fails. This can help the
             // app to recover when push settings has been modifed from another client
-            self.getPushRules().done(function(result) {
+            self.getPushRules().done(function (result) {
                 self.pushRules = result;
                 ruleRefreshDeferred.reject(err);
-            }, function(err2) {
+            }, function (err2) {
                 ruleRefreshDeferred.reject(err);
             });
         });
         return ruleRefreshDeferred.promise;
     }
 };
 
 // Search
@@ -2152,24 +3404,29 @@ MatrixClient.prototype.setRoomMutePushRu
  * @param {Object} opts Options for the search.
  * @param {string} opts.query The text to query.
  * @param {string=} opts.keys The keys to search on. Defaults to all keys. One
  * of "content.body", "content.name", "content.topic".
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.searchMessageText = function(opts, callback) {
+MatrixClient.prototype.searchMessageText = function (opts, callback) {
+    const roomEvents = {
+        search_term: opts.query
+    };
+
+    if ('keys' in opts) {
+        roomEvents.keys = opts.keys;
+    }
+
     return this.search({
         body: {
             search_categories: {
-                room_events: {
-                    keys: opts.keys,
-                    search_term: opts.query
-                }
+                room_events: roomEvents
             }
         }
     }, callback);
 };
 
 /**
  * Perform a server-side search for room events.
  *
@@ -2185,590 +3442,789 @@ MatrixClient.prototype.searchMessageText
  * Each entry in the results list is a {module:models/search-result.SearchResult}.
  *
  * @param {Object} opts
  * @param {string} opts.term     the term to search for
  * @param {Object} opts.filter   a JSON filter object to pass in the request
  * @return {module:client.Promise} Resolves: result object
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.searchRoomEvents = function(opts) {
+MatrixClient.prototype.searchRoomEvents = function (opts) {
     // TODO: support groups
 
-    var body = {
+    const body = {
         search_categories: {
             room_events: {
                 search_term: opts.term,
                 filter: opts.filter,
                 order_by: "recent",
                 event_context: {
                     before_limit: 1,
                     after_limit: 1,
-                    include_profile: true,
+                    include_profile: true
                 }
             }
         }
     };
 
-    var searchResults = {
+    const searchResults = {
         _query: body,
         results: [],
-        highlights: [],
+        highlights: []
     };
 
-    return this.search({body: body}).then(
-        this._processRoomEventsSearch.bind(this, searchResults)
-    );
+    return this.search({ body: body }).then(this._processRoomEventsSearch.bind(this, searchResults));
 };
 
 /**
  * Take a result from an earlier searchRoomEvents call, and backfill results.
  *
  * @param  {object} searchResults  the results object to be updated
  * @return {module:client.Promise} Resolves: updated result object
  * @return {Error} Rejects: with an error response.
  */
-MatrixClient.prototype.backPaginateRoomEventsSearch = function(searchResults) {
+MatrixClient.prototype.backPaginateRoomEventsSearch = function (searchResults) {
     // TODO: we should implement a backoff (as per scrollback()) to deal more
     // nicely with HTTP errors.
 
     if (!searchResults.next_batch) {
-        return q.reject(new Error("Cannot backpaginate event search any further"));
+        return _bluebird2.default.reject(new Error("Cannot backpaginate event search any further"));
     }
 
     if (searchResults.pendingRequest) {
         // already a request in progress - return the existing promise
         return searchResults.pendingRequest;
     }
 
-    var searchOpts = {
+    const searchOpts = {
         body: searchResults._query,
-        next_batch: searchResults.next_batch,
+        next_batch: searchResults.next_batch
     };
 
-    var promise = this.search(searchOpts).then(
-        this._processRoomEventsSearch.bind(this, searchResults)
-    ).finally(function() {
+    const promise = this.search(searchOpts).then(this._processRoomEventsSearch.bind(this, searchResults)).finally(function () {
         searchResults.pendingRequest = null;
     });
     searchResults.pendingRequest = promise;
 
     return promise;
 };
 
 /**
  * helper for searchRoomEvents and backPaginateRoomEventsSearch. Processes the
  * response from the API call and updates the searchResults
  *
  * @param {Object} searchResults
  * @param {Object} response
  * @return {Object} searchResults
  * @private
  */
-MatrixClient.prototype._processRoomEventsSearch = function(searchResults, response) {
-    var room_events = response.search_categories.room_events;
+MatrixClient.prototype._processRoomEventsSearch = function (searchResults, response) {
+    const room_events = response.search_categories.room_events;
 
     searchResults.count = room_events.count;
     searchResults.next_batch = room_events.next_batch;
 
     // combine the highlight list with our existing list; build an object
     // to avoid O(N^2) fail
-    var highlights = {};
-    room_events.highlights.forEach(function(hl) { highlights[hl] = 1; });
-    searchResults.highlights.forEach(function(hl) { highlights[hl] = 1; });
+    const highlights = {};
+    room_events.highlights.forEach(function (hl) {
+        highlights[hl] = 1;
+    });
+    searchResults.highlights.forEach(function (hl) {
+        highlights[hl] = 1;
+    });
 
     // turn it back into a list.
     searchResults.highlights = Object.keys(highlights);
 
     // append the new results to our existing results
-    for (var i = 0; i < room_events.results.length; i++) {
-        var sr = SearchResult.fromJson(room_events.results[i], this.getEventMapper());
+    for (let i = 0; i < room_events.results.length; i++) {
+        const sr = SearchResult.fromJson(room_events.results[i], this.getEventMapper());
         searchResults.results.push(sr);
     }
     return searchResults;
 };
 
-
 /**
  * Populate the store with rooms the user has left.
  * @return {module:client.Promise} Resolves: TODO - Resolved when the rooms have
  * been added to the data store.
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.syncLeftRooms = function() {
+MatrixClient.prototype.syncLeftRooms = function () {
     // Guard against multiple calls whilst ongoing and multiple calls post success
     if (this._syncedLeftRooms) {
-        return q([]); // don't call syncRooms again if it succeeded.
+        return _bluebird2.default.resolve([]); // don't call syncRooms again if it succeeded.
     }
     if (this._syncLeftRoomsPromise) {
         return this._syncLeftRoomsPromise; // return the ongoing request
     }
-    var self = this;
-    var syncApi = new SyncApi(this, this._clientOpts);
+    const self = this;
+    const syncApi = new SyncApi(this, this._clientOpts);
     this._syncLeftRoomsPromise = syncApi.syncLeftRooms();
 
     // cleanup locks
-    this._syncLeftRoomsPromise.then(function(res) {
-        console.log("Marking success of sync left room request");
+    this._syncLeftRoomsPromise.then(function (res) {
+        _logger2.default.log("Marking success of sync left room request");
         self._syncedLeftRooms = true; // flip the bit on success
-    }).finally(function() {
+    }).finally(function () {
         self._syncLeftRoomsPromise = null; // cleanup ongoing request state
     });
 
     return this._syncLeftRoomsPromise;
 };
 
 // Filters
 // =======
 
 /**
  * Create a new filter.
  * @param {Object} content The HTTP body for the request
  * @return {Filter} Resolves to a Filter object.
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.createFilter = function(content) {
-    var self = this;
-    var path = utils.encodeUri("/user/$userId/filter", {
+MatrixClient.prototype.createFilter = function (content) {
+    const self = this;
+    const path = utils.encodeUri("/user/$userId/filter", {
         $userId: this.credentials.userId
     });
-    return this._http.authedRequest(
-        undefined, "POST", path, undefined, content
-    ).then(function(response) {
+    return this._http.authedRequest(undefined, "POST", path, undefined, content).then(function (response) {
         // persist the filter
-        var filter = Filter.fromJson(
-            self.credentials.userId, response.filter_id, content
-        );
+        const filter = Filter.fromJson(self.credentials.userId, response.filter_id, content);
         self.store.storeFilter(filter);
         return filter;
     });
 };
 
 /**
  * Retrieve a filter.
  * @param {string} userId The user ID of the filter owner
  * @param {string} filterId The filter ID to retrieve
  * @param {boolean} allowCached True to allow cached filters to be returned.
  * Default: True.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
+MatrixClient.prototype.getFilter = function (userId, filterId, allowCached) {
     if (allowCached) {
-        var filter = this.store.getFilter(userId, filterId);
+        const filter = this.store.getFilter(userId, filterId);
         if (filter) {
-            return q(filter);
+            return _bluebird2.default.resolve(filter);
         }
     }
 
-    var self = this;
-    var path = utils.encodeUri("/user/$userId/filter/$filterId", {
+    const self = this;
+    const path = utils.encodeUri("/user/$userId/filter/$filterId", {
         $userId: userId,
         $filterId: filterId
     });
 
-    return this._http.authedRequest(
-        undefined, "GET", path, undefined, undefined
-    ).then(function(response) {
+    return this._http.authedRequest(undefined, "GET", path, undefined, undefined).then(function (response) {
         // persist the filter
-        var filter = Filter.fromJson(
-            userId, filterId, response
-        );
+        const filter = Filter.fromJson(userId, filterId, response);
         self.store.storeFilter(filter);
         return filter;
     });
 };
 
 /**
  * @param {string} filterName
  * @param {Filter} filter
  * @return {Promise<String>} Filter ID
  */
-MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
-
-    var filterId = this.store.getFilterIdByName(filterName);
-    var promise = q();
-    var self = this;
+MatrixClient.prototype.getOrCreateFilter = function (filterName, filter) {
+    const filterId = this.store.getFilterIdByName(filterName);
+    let promise = _bluebird2.default.resolve();
+    const self = this;
 
     if (filterId) {
         // check that the existing filter matches our expectations
-        promise = self.getFilter(self.credentials.userId,
-                         filterId, true
-        ).then(function(existingFilter) {
-            var oldDef = existingFilter.getDefinition();
-            var newDef = filter.getDefinition();
+        promise = self.getFilter(self.credentials.userId, filterId, true).then(function (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));
-                return q(filterId);
+                return _bluebird2.default.resolve(filterId);
             }
             // debuglog("Existing filter ID %s: %s; new filter: %s",
             //          filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
             self.store.setFilterIdByName(filterName, undefined);
             return undefined;
-        }, function(error) {
+        }, function (error) {
             // Synapse currently returns the following when the filter cannot be found:
             // {
             //     errcode: "M_UNKNOWN",
             //     name: "M_UNKNOWN",
             //     message: "No row found",
             //     data: Object, httpStatus: 404
             // }
-            if (error.httpStatus === 404 &&
-                (error.errcode === "M_UNKNOWN" || error.errcode === "M_NOT_FOUND")) {
+            if (error.httpStatus === 404 && (error.errcode === "M_UNKNOWN" || error.errcode === "M_NOT_FOUND")) {
                 // Clear existing filterId from localStorage
                 // if it no longer exists on the server
                 self.store.setFilterIdByName(filterName, undefined);
                 // Return a undefined value for existingId further down the promise chain
                 return undefined;
             } else {
                 throw error;
             }
         });
     }
 
-    return promise.then(function(existingId) {
+    return promise.then(function (existingId) {
         if (existingId) {
             return existingId;
         }
 
         // create a new filter
-        return self.createFilter(filter.getDefinition()
-        ).then(function(createdFilter) {
+        return self.createFilter(filter.getDefinition()).then(function (createdFilter) {
             // debuglog("Created new filter ID %s: %s", createdFilter.filterId,
             //          JSON.stringify(createdFilter.getDefinition()));
             self.store.setFilterIdByName(filterName, createdFilter.filterId);
             return createdFilter.filterId;
         });
     });
 };
 
-
 /**
  * Gets a bearer token from the Home Server that the user can
  * present to a third party in order to prove their ownership
  * of the Matrix account they are logged into.
  * @return {module:client.Promise} Resolves: Token object
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.getOpenIdToken = function() {
-    var path = utils.encodeUri("/user/$userId/openid/request_token", {
-        $userId: this.credentials.userId,
+MatrixClient.prototype.getOpenIdToken = function () {
+    const path = utils.encodeUri("/user/$userId/openid/request_token", {
+        $userId: this.credentials.userId
     });
 
-    return this._http.authedRequest(
-        undefined, "POST", path, undefined, {}
-    );
+    return this._http.authedRequest(undefined, "POST", path, undefined, {});
 };
 
-
 // VoIP operations
 // ===============
 
 /**
  * @param {module:client.callback} callback Optional.
  * @return {module:client.Promise} Resolves: TODO
  * @return {module:http-api.MatrixError} Rejects: with an error response.
  */
-MatrixClient.prototype.turnServer = function(callback) {
+MatrixClient.prototype.turnServer = function (callback) {
     return this._http.authedRequest(callback, "GET", "/voip/turnServer");
 };
 
 /**
  * Get the TURN servers for this home server.
  * @return {Array<Object>} The servers or an empty list.
  */
-MatrixClient.prototype.getTurnServers = function() {
+MatrixClient.prototype.getTurnServers = function () {
     return this._turnServers || [];
 };
 
+/**
+ * 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.
+ *
+ * @param {boolean} allow
+ */
+MatrixClient.prototype.setFallbackICEServerAllowed = function (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 {boolean}
+ */
+MatrixClient.prototype.isFallbackICEServerAllowed = function () {
+    return this._fallbackICEServerAllowed;
+};
+
+// Synapse-specific APIs
+// =====================
+
+/**
+ * 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>
+ * @return {boolean} true if the user appears to be a Synapse administrator.
+ */
+MatrixClient.prototype.isSynapseAdministrator = function () {
+    return this.whoisSynapseUser(this.getUserId()).then(() => true).catch(() => false);
+};
+
+/**
+ * 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 {string} userId the User ID to look up.
+ * @return {object} the whois response - see Synapse docs for information.
+ */
+MatrixClient.prototype.whoisSynapseUser = function (userId) {
+    const path = utils.encodeUri("/_synapse/admin/v1/whois/$userId", { $userId: userId });
+    return this._http.authedRequest(undefined, '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 {string} userId the User ID to deactivate.
+ * @return {object} the deactivate response - see Synapse docs for information.
+ */
+MatrixClient.prototype.deactivateSynapseUser = function (userId) {
+    const path = utils.encodeUri("/_synapse/admin/v1/deactivate/$userId", { $userId: userId });
+    return this._http.authedRequest(undefined, 'POST', path, undefined, undefined, { prefix: '' });
+};
+
 // Higher level APIs
 // =================
 
 // TODO: stuff to handle:
 //   local echo
 //   event dup suppression? - apparently we should still be doing this
 //   tracking current display name / avatar per-message
 //   pagination
 //   re-sending (including persisting pending messages to be sent)
 //   - Need a nice way to callback the app for arbitrary events like
 //     displayname changes
 //   due to ambiguity (or should this be on a chat-specific layer)?
 //   reconnect after connectivity outages
 
 
 /**
- * High level helper method to call initialSync, emit the resulting events,
- * and then start polling the eventStream for new events. To listen for these
+ * High level helper method to begin syncing and poll for new events. To listen for these
  * events, add a listener for {@link module:client~MatrixClient#event:"event"}
- * via {@link module:client~MatrixClient#on}.
+ * via {@link module:client~MatrixClient#on}. Alternatively, listen for specific
+ * state change events.
  * @param {Object=} opts Options to apply when syncing.
  * @param {Number=} opts.initialSyncLimit The event <code>limit=</code> to apply
  * to initial sync. Default: 8.
  * @param {Boolean=} opts.includeArchivedRooms True to put <code>archived=true</code>
  * on the <code>/initialSync</code> request. Default: false.
  * @param {Boolean=} opts.resolveInvitesToProfiles True to do /profile requests
  * on every invite event if the displayname/avatar_url is not known for this user ID.
  * Default: false.
  *
  * @param {String=} opts.pendingEventOrdering Controls where pending messages
  * appear in a room's timeline. If "<b>chronological</b>", messages will appear
  * in the timeline when the call to <code>sendEvent</code> was made. If
  * "<b>detached</b>", pending messages will appear in a separate list,
  * accessbile via {@link module:models/room#getPendingEvents}. Default:
  * "chronological".
  *
- * @param {Number=} opts.pollTimeout The number of milliseconds to wait on /events.
+ * @param {Number=} opts.pollTimeout The number of milliseconds to wait on /sync.
  * Default: 30000 (30 seconds).
  *
  * @param {Filter=} opts.filter The filter to apply to /sync calls. This will override
  * the opts.initialSyncLimit, which would normally result in a timeline limit filter.
+ *
+ * @param {Boolean=} opts.disablePresence True to perform syncing without automatically
+ * updating presence.
+ * @param {Boolean=} opts.lazyLoadMembers True to not load all membership events during
+ * initial sync but fetch them when needed by calling `loadOutOfBandMembers`
+ * This will override the filter option at this moment.
  */
-MatrixClient.prototype.startClient = function(opts) {
+MatrixClient.prototype.startClient = async function (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
         };
     }
 
-    this._clientOpts = opts;
-
     if (this._crypto) {
-        this._crypto.uploadKeys(5).done();
-        var tenMinutes = 1000 * 60 * 10;
-        var self = this;
-        this._uploadIntervalID = global.setInterval(function() {
-            self._crypto.uploadKeys(5).done();
-        }, tenMinutes);
+        this._crypto.uploadDeviceKeys().done();
+        this._crypto.start();
     }
 
     // periodically poll for turn servers if we support voip
     checkTurnServers(this);
 
     if (this._syncApi) {
         // This shouldn't happen since we thought the client was not running
-        console.error("Still have sync object whilst not running: stopping old one");
+        _logger2.default.error("Still have sync object whilst not running: stopping old one");
         this._syncApi.stop();
     }
+
+    // shallow-copy the opts dict before modifying and storing it
+    opts = Object.assign({}, opts);
+
+    opts.crypto = this._crypto;
+    opts.canResetEntireTimeline = roomId => {
+        if (!this._canResetTimelineCallback) {
+            return false;
+        }
+        return this._canResetTimelineCallback(roomId);
+    };
+    this._clientOpts = opts;
     this._syncApi = new SyncApi(this, opts);
     this._syncApi.sync();
 };
 
 /**
+ * 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 {object} opts the complete set of client options
+ * @return {Promise} for store operation */
+MatrixClient.prototype._storeClientOptions = function () {
+    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);
+};
+
+/**
  * High level helper method to stop the client from polling and allow a
  * clean shutdown.
  */
-MatrixClient.prototype.stopClient = function() {
+MatrixClient.prototype.stopClient = function () {
+    _logger2.default.log('stopping MatrixClient');
+
     this.clientRunning = false;
     // TODO: f.e. Room => self.store.storeRoom(room) ?
     if (this._syncApi) {
         this._syncApi.stop();
         this._syncApi = null;
     }
     if (this._crypto) {
-        global.clearInterval(this._uploadIntervalID);
+        this._crypto.stop();
+    }
+    if (this._peekSync) {
+        this._peekSync.stopPeeking();
     }
     global.clearTimeout(this._checkTurnServersTimeoutID);
 };
 
+/**
+ * Get the API versions supported by the server, along with any
+ * unstable APIs it supports
+ * @return {Promise<object>} The server /versions response
+ */
+MatrixClient.prototype.getVersions = async function () {
+    if (this._serverVersionsCache === null) {
+        this._serverVersionsCache = await this._http.request(undefined, // callback
+        "GET", "/_matrix/client/versions", undefined, // queryParams
+        undefined, // data
+        {
+            prefix: ''
+        });
+    }
+    return this._serverVersionsCache;
+};
+
+/**
+ * Query the server to see if it support members lazy loading
+ * @return {Promise<boolean>} true if server supports lazy loading
+ */
+MatrixClient.prototype.doesServerSupportLazyLoading = async function () {
+    const response = await this.getVersions();
+
+    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.
+ * @return {Promise<boolean>} true if id_server parameter is required
+ */
+MatrixClient.prototype.doesServerRequireIdServerParam = async function () {
+    const response = await this.getVersions();
+
+    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["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.
+ * @return {Promise<boolean>} true if id_access_token can be sent
+ */
+MatrixClient.prototype.doesServerAcceptIdentityAccessToken = async function () {
+    const response = await this.getVersions();
+
+    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.
+ * @return {Promise<boolean>} true if separate functions are supported
+ */
+MatrixClient.prototype.doesServerSupportSeparateAddAndBind = async function () {
+    const response = await this.getVersions();
+
+    const versions = response["versions"];
+    const unstableFeatures = response["unstable_features"];
+
+    return versions && versions.includes("r0.6.0") || unstableFeatures && unstableFeatures["m.separate_add_and_bind"];
+};
+
+/**
+ * Get if lazy loading members is being used.
+ * @return {boolean} Whether or not members are lazy loaded by this client
+ */
+MatrixClient.prototype.hasLazyLoadMembersEnabled = function () {
+    return !!this._clientOpts.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 {Function} cb The callback which will be invoked.
+ */
+MatrixClient.prototype.setCanResetTimelineCallback = function (cb) {
+    this._canResetTimelineCallback = cb;
+};
+
+/**
+ * Get the callback set via `setCanResetTimelineCallback`.
+ * @return {?Function} The callback or null
+ */
+MatrixClient.prototype.getCanResetTimelineCallback = function () {
+    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 {string} roomId the room of the event
+ * @param {string} eventId the id of the event
+ * @param {string} relationType the rel_type of the relations requested
+ * @param {string} eventType the event type of the relations requested
+ * @param {Object} opts options with optional values for the request.
+ * @param {Object} opts.from the pagination token returned from a previous request as `nextBatch` to return following relations.
+ * @return {Object} an object with `events` as `MatrixEvent[]` and optionally `nextBatch` if more relations are available.
+ */
+MatrixClient.prototype.relations = async function (roomId, eventId, relationType, eventType, opts = {}) {
+    const fetchedEventType = _getEncryptedIfNeededEventType(this, roomId, eventType);
+    const result = await this.fetchRelations(roomId, eventId, relationType, fetchedEventType, opts);
+    const mapper = this.getEventMapper();
+    let originalEvent;
+    if (result.original_event) {
+        originalEvent = mapper(result.original_event);
+    }
+    let events = result.chunk.map(mapper);
+    if (fetchedEventType === "m.room.encrypted") {
+        const allEvents = originalEvent ? events.concat(originalEvent) : events;
+        await _bluebird2.default.all(allEvents.map(e => {
+            return new _bluebird2.default(resolve => e.once("Event.decrypted", resolve));
+        }));
+        events = events.filter(e => e.getType() === eventType);
+    }
+    return {
+        originalEvent,
+        events,
+        nextBatch: result.next_batch
+    };
+};
+
 function setupCallEventHandler(client) {
-    var candidatesByCall = {
+    const candidatesByCall = {
         // callId: [Candidate]
     };
 
     // Maintain a buffer of events before the client has synced for the first time.
     // This buffer will be inspected to see if we should send incoming call
     // notifications. It needs to be buffered to correctly determine if an
     // incoming call has had a matching answer/hangup.
-    var callEventBuffer = [];
-    var isClientPrepared = false;
-    client.on("sync", function(state) {
+    let callEventBuffer = [];
+    let isClientPrepared = false;
+    client.on("sync", function (state) {
         if (state === "PREPARED") {
             isClientPrepared = true;
-            var ignoreCallIds = {}; // Set<String>
+            const ignoreCallIds = {}; // Set<String>
             // inspect the buffer and mark all calls which have been answered
             // or hung up before passing them to the call event handler.
-            for (var i = callEventBuffer.length - 1; i >= 0; i--) {
-                var ev = callEventBuffer[i];
-                if (ev.getType() === "m.call.answer" ||
-                        ev.getType() === "m.call.hangup") {
+            for (let i = callEventBuffer.length - 1; i >= 0; i--) {
+                const ev = callEventBuffer[i];
+                if (ev.getType() === "m.call.answer" || ev.getType() === "m.call.hangup") {
                     ignoreCallIds[ev.getContent().call_id] = "yep";
                 }
             }
             // now loop through the buffer chronologically and inject them
-            callEventBuffer.forEach(function(e) {
+            callEventBuffer.forEach(function (e) {
                 if (ignoreCallIds[e.getContent().call_id]) {
+                    // This call has previously been ansered or hung up: ignore it
                     return;
                 }
                 callEventHandler(e);
             });
             callEventBuffer = [];
         }
     });
 
-    client.on("event", function(event) {
+    client.on("event", onEvent);
+
+    function onEvent(event) {
+        if (event.getType().indexOf("m.call.") !== 0) {
+            // not a call event
+            if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
+                // not *yet* a call event, but might become one...
+                event.once("Event.decrypted", onEvent);
+            }
+            return;
+        }
         if (!isClientPrepared) {
-            if (event.getType().indexOf("m.call.") === 0) {
-                callEventBuffer.push(event);
-            }
+            callEventBuffer.push(event);
             return;
         }
         callEventHandler(event);
-    });
+    }
 
     function callEventHandler(event) {
-        if (event.getType().indexOf("m.call.") !== 0) {
-            return; // not a call event
-        }
-        var content = event.getContent();
-        var call = content.call_id ? client.callList[content.call_id] : undefined;
-        var i;
+        const content = event.getContent();
+        let call = content.call_id ? client.callList[content.call_id] : undefined;
+        let i;
         //console.log("RECV %s content=%s", event.getType(), JSON.stringify(content));
 
         if (event.getType() === "m.call.invite") {
             if (event.getSender() === client.credentials.userId) {
                 return; // ignore invites you send
             }
 
             if (event.getAge() > content.lifetime) {
                 return; // expired call
             }
 
             if (call && call.state === "ended") {
                 return; // stale/old invite event
             }
             if (call) {
-                console.log(
-                    "WARN: Already have a MatrixCall with id %s but got an " +
-                    "invite. Clobbering.",
-                    content.call_id
-                );
+                _logger2.default.log("WARN: Already have a MatrixCall with id %s but got an " + "invite. Clobbering.", content.call_id);
             }
 
-            call = webRtcCall.createNewMatrixCall(client, event.getRoomId());
+            call = webRtcCall.createNewMatrixCall(client, event.getRoomId(), {
+                forceTURN: client._forceTURN
+            });
             if (!call) {
-                console.log(
-                    "Incoming call ID " + content.call_id + " but this client " +
-                    "doesn't support WebRTC"
-                );
+                _logger2.default.log("Incoming call ID " + content.call_id + " but this client " + "doesn't support WebRTC");
                 // don't hang up the call: there could be other clients
                 // connected that do support WebRTC and declining the
                 // the call on their behalf would be really annoying.
                 return;
             }
 
             call.callId = content.call_id;
             call._initWithInvite(event);
             client.callList[call.callId] = call;
 
             // if we stashed candidate events for that call ID, play them back now
             if (candidatesByCall[call.callId]) {
                 for (i = 0; i < candidatesByCall[call.callId].length; i++) {
-                    call._gotRemoteIceCandidate(
-                        candidatesByCall[call.callId][i]
-                    );
+                    call._gotRemoteIceCandidate(candidatesByCall[call.callId][i]);
                 }
             }
 
             // Were we trying to call that user (room)?
-            var existingCall;
-            var existingCalls = utils.values(client.callList);
+            let existingCall;
+            const existingCalls = utils.values(client.callList);
             for (i = 0; i < existingCalls.length; ++i) {
-                var thisCall = existingCalls[i];
-                if (call.room_id === thisCall.room_id &&
-                        thisCall.direction === 'outbound' &&
-                        (["wait_local_media", "create_offer", "invite_sent"].indexOf(
-                            thisCall.state) !== -1)) {
+                const thisCall = existingCalls[i];
+                if (call.roomId === thisCall.roomId && thisCall.direction === 'outbound' && ["wait_local_media", "create_offer", "invite_sent"].indexOf(thisCall.state) !== -1) {
                     existingCall = thisCall;
                     break;
                 }
             }
 
             if (existingCall) {
                 // If we've only got to wait_local_media or create_offer and
                 // we've got an invite, pick the incoming call because we know
                 // we haven't sent our invite yet otherwise, pick whichever
                 // call has the lowest call ID (by string comparison)
-                if (existingCall.state === 'wait_local_media' ||
-                        existingCall.state === 'create_offer' ||
-                        existingCall.callId > call.callId) {
-                    console.log(
-                        "Glare detected: answering incoming call " + call.callId +
-                        " and canceling outgoing call " + existingCall.callId
-                    );
+                if (existingCall.state === 'wait_local_media' || existingCall.state === 'create_offer' || existingCall.callId > call.callId) {
+                    _logger2.default.log("Glare detected: answering incoming call " + call.callId + " and canceling outgoing call " + existingCall.callId);
                     existingCall._replacedBy(call);
                     call.answer();
-                }
-                else {
-                    console.log(
-                        "Glare detected: rejecting incoming call " + call.callId +
-                        " and keeping outgoing call " + existingCall.callId
-                    );
+                } else {
+                    _logger2.default.log("Glare detected: rejecting incoming call " + call.callId + " and keeping outgoing call " + existingCall.callId);
                     call.hangup();
                 }
-            }
-            else {
+            } else {
                 client.emit("Call.incoming", call);
             }
-        }
-        else if (event.getType() === 'm.call.answer') {
+        } else if (event.getType() === 'm.call.answer') {
             if (!call) {
                 return;
             }
             if (event.getSender() === client.credentials.userId) {
                 if (call.state === 'ringing') {
                     call._onAnsweredElsewhere(content);
                 }
-            }
-            else {
+            } else {
                 call._receivedAnswer(content);
             }
-        }
-        else if (event.getType() === 'm.call.candidates') {
+        } else if (event.getType() === 'm.call.candidates') {
             if (event.getSender() === client.credentials.userId) {
                 return;
             }
             if (!call) {
                 // store the candidates; we may get a call eventually.
                 if (!candidatesByCall[content.call_id]) {
                     candidatesByCall[content.call_id] = [];
                 }
-                candidatesByCall[content.call_id] = candidatesByCall[
-                    content.call_id
-                ].concat(content.candidates);
-            }
-            else {
+                candidatesByCall[content.call_id] = candidatesByCall[content.call_id].concat(content.candidates);
+            } else {
                 for (i = 0; i < content.candidates.length; i++) {
                     call._gotRemoteIceCandidate(content.candidates[i]);
                 }
             }
-        }
-        else if (event.getType() === 'm.call.hangup') {
+        } else if (event.getType() === 'm.call.hangup') {
             // Note that we also observe our own hangups here so we can see
             // if we've already rejected a call that would otherwise be valid
             if (!call) {
                 // if not live, store the fact that the call has ended because
                 // we're probably getting events backwards so
                 // the hangup will come before the invite
                 call = webRtcCall.createNewMatrixCall(client, event.getRoomId());
                 if (call) {
                     call.callId = content.call_id;
                     call._initWithHangup(event);
                     client.callList[content.call_id] = call;
                 }
-            }
-            else {
+            } else {
                 if (call.state !== 'ended') {
                     call._onHangupReceived(content);
                     delete client.callList[content.call_id];
                 }
             }
         }
     }
 }
@@ -2776,38 +4232,37 @@ function setupCallEventHandler(client) {
 function checkTurnServers(client) {
     if (!client._supportsVoip) {
         return;
     }
     if (client.isGuest()) {
         return; // guests can't access TURN servers
     }
 
-    client.turnServer().done(function(res) {
+    client.turnServer().done(function (res) {
         if (res.uris) {
-            console.log("Got TURN URIs: " + res.uris + " refresh in " +
-                res.ttl + " secs");
+            _logger2.default.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs");
             // map the response to a format that can be fed to
             // RTCPeerConnection
-            var servers = {
+            const servers = {
                 urls: res.uris,
                 username: res.username,
                 credential: res.password
             };
             client._turnServers = [servers];
             // re-fetch when we're about to reach the TTL
-            client._checkTurnServersTimeoutID =
-                setTimeout(function() { checkTurnServers(client); },
-                           (res.ttl || (60 * 60)) * 1000 * 0.9
-                          );
+            client._checkTurnServersTimeoutID = setTimeout(() => {
+                checkTurnServers(client);
+            }, (res.ttl || 60 * 60) * 1000 * 0.9);
         }
-    }, function(err) {
-        console.error("Failed to get TURN URIs");
-        client._checkTurnServersTimeoutID =
-            setTimeout(function() { checkTurnServers(client); }, 60000);
+    }, function (err) {
+        _logger2.default.error("Failed to get TURN URIs");
+        client._checkTurnServersTimeoutID = setTimeout(function () {
+            checkTurnServers(client);
+        }, 60000);
     });
 }
 
 function _reject(callback, defer, err) {
     if (callback) {
         callback(err);
     }
     defer.reject(err);
@@ -2817,49 +4272,47 @@ function _resolve(callback, defer, res) 
     if (callback) {
         callback(null, res);
     }
     defer.resolve(res);
 }
 
 function _PojoToMatrixEventMapper(client) {
     function mapper(plainOldJsObject) {
-        var event = new MatrixEvent(plainOldJsObject);
+        const event = new MatrixEvent(plainOldJsObject);
         if (event.isEncrypted()) {
-            _decryptEvent(client, event);
+            client.reEmitter.reEmit(event, ["Event.decrypted"]);
+            event.attemptDecryption(client._crypto);
+        }
+        const room = client.getRoom(event.getRoomId());
+        if (room) {
+            room.reEmitter.reEmit(event, ["Event.replaced"]);
         }
         return event;
     }
     return mapper;
 }
 
 /**
  * @return {Function}
  */
-MatrixClient.prototype.getEventMapper = function() {
+MatrixClient.prototype.getEventMapper = function () {
     return _PojoToMatrixEventMapper(this);
 };
 
 // Identity Server Operations
 // ==========================
 
 /**
  * Generates a random string suitable for use as a client secret. <strong>This
  * method is experimental and may change.</strong>
  * @return {string} A new client secret
  */
-MatrixClient.prototype.generateClientSecret = function() {
-    var ret = "";
-    var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
-
-    for (var i = 0; i < 32; i++) {
-        ret += chars.charAt(Math.floor(Math.random() * chars.length));
-    }
-
-    return ret;
+MatrixClient.prototype.generateClientSecret = function () {
+    return (0, _randomstring.randomString)(32);
 };
 
 /** */
 module.exports.MatrixClient = MatrixClient;
 /** */
 module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
 
 // MatrixClient Event JSDocs
@@ -2886,77 +4339,119 @@ module.exports.CRYPTO_ENABLED = CRYPTO_E
  * matrixClient.on("toDeviceEvent", function(event){
  *   var sender = event.getSender();
  * });
  */
 
 /**
  * Fires whenever the SDK's syncing state is updated. The state can be one of:
  * <ul>
- * <li>PREPARED : The client has synced with the server at least once and is
+ *
+ * <li>PREPARED: The client has synced with the server at least once and is
  * ready for methods to be called on it. This will be immediately followed by
  * a state of SYNCING. <i>This is the equivalent of "syncComplete" in the
  * previous API.</i></li>
+ *
+ * <li>CATCHUP: The client has detected the connection to the server might be
+ * available again and will now try to do a sync again. As this sync might take
+ * a long time (depending how long ago was last synced, and general server
+ * performance) the client is put in this mode so the UI can reflect trying
+ * to catch up with the server after losing connection.</li>
+ *
  * <li>SYNCING : The client is currently polling for new events from the server.
  * This will be called <i>after</i> processing latest events from a sync.</li>
+ *
  * <li>ERROR : The client has had a problem syncing with the server. If this is
  * called <i>before</i> PREPARED then there was a problem performing the initial
  * sync. If this is called <i>after</i> PREPARED then there was a problem polling
  * the server for updates. This may be called multiple times even if the state is
  * already ERROR. <i>This is the equivalent of "syncError" in the previous
  * API.</i></li>
- * <li>RECONNECTING: The sync connedtion has dropped, but not in a way that should
- * be considered erroneous.
+ *
+ * <li>RECONNECTING: The sync connection has dropped, but not (yet) in a way that
+ * should be considered erroneous.
  * </li>
+ *
  * <li>STOPPED: The client has stopped syncing with server due to stopClient
  * being called.
  * </li>
  * </ul>
  * State transition diagram:
  * <pre>
  *                                          +---->STOPPED
  *                                          |
  *              +----->PREPARED -------> SYNCING <--+
- *              |        ^                  ^       |
- *              |        |                  |       |
- *              |        |                  V       |
- *   null ------+        |  +-RECONNECTING<-+       |
- *              |        |  V                       |
+ *              |                        ^  |  ^    |
+ *              |      CATCHUP ----------+  |  |    |
+ *              |        ^                  V  |    |
+ *   null ------+        |  +------- RECONNECTING   |
+ *              |        V  V                       |
  *              +------->ERROR ---------------------+
  *
  * NB: 'null' will never be emitted by this event.
+ *
  * </pre>
  * Transitions:
  * <ul>
+ *
  * <li><code>null -> PREPARED</code> : Occurs when the initial sync is completed
  * first time. This involves setting up filters and obtaining push rules.
+ *
  * <li><code>null -> ERROR</code> : Occurs when the initial sync failed first time.
+ *
  * <li><code>ERROR -> PREPARED</code> : Occurs when the initial sync succeeds
  * after previously failing.
+ *
  * <li><code>PREPARED -> SYNCING</code> : Occurs immediately after transitioning
  * to PREPARED. Starts listening for live updates rather than catching up.
- * <li><code>SYNCING -> ERROR</code> : Occurs the first time a client cannot perform a
- * live update.
+ *
+ * <li><code>SYNCING -> RECONNECTING</code> : Occurs when the live update fails.
+ *
+ * <li><code>RECONNECTING -> RECONNECTING</code> : Can occur if the update calls
+ * continue to fail, but the keepalive calls (to /versions) succeed.
+ *
+ * <li><code>RECONNECTING -> ERROR</code> : Occurs when the keepalive call also fails
+ *
  * <li><code>ERROR -> SYNCING</code> : Occurs when the client has performed a
  * live update after having previously failed.
- * <li><code>ERROR -> ERROR</code> : Occurs when the client has failed to sync
+ *
+ * <li><code>ERROR -> ERROR</code> : Occurs when the client has failed to keepalive
  * for a second time or more.</li>
+ *
  * <li><code>SYNCING -> SYNCING</code> : Occurs when the client has performed a live
  * update. This is called <i>after</i> processing.</li>
+ *
  * <li><code>* -> STOPPED</code> : Occurs once the client has stopped syncing or
  * trying to sync after stopClient has been called.</li>
  * </ul>
  *
  * @event module:client~MatrixClient#"sync"
+ *
  * @param {string} state An enum representing the syncing state. One of "PREPARED",
  * "SYNCING", "ERROR", "STOPPED".
+ *
  * @param {?string} prevState An enum representing the previous syncing state.
  * One of "PREPARED", "SYNCING", "ERROR", "STOPPED" <b>or null</b>.
+ *
  * @param {?Object} data Data about this transition.
- * @param {MatrixError} data.err The matrix error if <code>state=ERROR</code>.
+ *
+ * @param {MatrixError} data.error The matrix error if <code>state=ERROR</code>.
+ *
+ * @param {String} data.oldSyncToken The 'since' token passed to /sync.
+ *    <code>null</code> for the first successful sync since this client was
+ *    started. Only present if <code>state=PREPARED</code> or
+ *    <code>state=SYNCING</code>.
+ *
+ * @param {String} data.nextSyncToken The 'next_batch' result from /sync, which
+ *    will become the 'since' token for the next call to /sync. Only present if
+ *    <code>state=PREPARED</code> or <code>state=SYNCING</code>.
+ *
+ * @param {boolean} data.catchingUp True if we are working our way through a
+ *    backlog of events after connecting. Only present if <code>state=SYNCING</code>.
+ *
  * @example
  * matrixClient.on("sync", function(state, prevState, data) {
  *   switch (state) {
  *     case "ERROR":
  *       // update UI to say "Connection Lost"
  *       break;
  *     case "SYNCING":
  *       // update UI to remove any "Connection Lost" message
@@ -2964,38 +4459,49 @@ module.exports.CRYPTO_ENABLED = CRYPTO_E
  *     case "PREPARED":
  *       // the client instance is ready to be queried.
  *       var rooms = matrixClient.getRooms();
  *       break;
  *   }
  * });
  */
 
- /**
- * Fires whenever a new Room is added. This will fire when you are invited to a
- * room, as well as when you join a room. <strong>This event is experimental and
- * may change.</strong>
- * @event module:client~MatrixClient#"Room"
- * @param {Room} room The newly created, fully populated room.
- * @example
- * matrixClient.on("Room", function(room){
- *   var roomId = room.roomId;
- * });
- */
-
- /**
- * Fires whenever a Room is removed. This will fire when you forget a room.
- * <strong>This event is experimental and may change.</strong>
- * @event module:client~MatrixClient#"deleteRoom"
- * @param {string} roomId The deleted room ID.
- * @example
- * matrixClient.on("deleteRoom", function(roomId){
- *   // update UI from getRooms()
- * });
- */
+/**
+* Fires whenever the sdk learns about a new group. <strong>This event
+* is experimental and may change.</strong>
+* @event module:client~MatrixClient#"Group"
+* @param {Group} group The newly created, fully populated group.
+* @example
+* matrixClient.on("Group", function(group){
+*   var groupId = group.groupId;
+* });
+*/
+
+/**
+* Fires whenever a new Room is added. This will fire when you are invited to a
+* room, as well as when you join a room. <strong>This event is experimental and
+* may change.</strong>
+* @event module:client~MatrixClient#"Room"
+* @param {Room} room The newly created, fully populated room.
+* @example
+* matrixClient.on("Room", function(room){
+*   var roomId = room.roomId;
+* });
+*/
+
+/**
+* Fires whenever a Room is removed. This will fire when you forget a room.
+* <strong>This event is experimental and may change.</strong>
+* @event module:client~MatrixClient#"deleteRoom"
+* @param {string} roomId The deleted room ID.
+* @example
+* matrixClient.on("deleteRoom", function(roomId){
+*   // update UI from getRooms()
+* });
+*/
 
 /**
  * Fires whenever an incoming call arrives.
  * @event module:client~MatrixClient#"Call.incoming"
  * @param {module:webrtc/call~MatrixCall} call The incoming call.
  * @example
  * matrixClient.on("Call.incoming", function(call){
  *   call.answer(); // auto-answer
@@ -3004,41 +4510,98 @@ module.exports.CRYPTO_ENABLED = CRYPTO_E
 
 /**
  * Fires whenever the login session the JS SDK is using is no
  * longer valid and the user must log in again.
  * NB. This only fires when action is required from the user, not
  * when then login session can be renewed by using a refresh token.
  * @event module:client~MatrixClient#"Session.logged_out"
  * @example
- * matrixClient.on("Session.logged_out", function(call){
+ * matrixClient.on("Session.logged_out", function(errorObj){
  *   // show the login screen
  * });
  */
 
 /**
+ * Fires when the JS SDK receives a M_CONSENT_NOT_GIVEN error in response
+ * to a HTTP request.
+ * @event module:client~MatrixClient#"no_consent"
+ * @example
+ * matrixClient.on("no_consent", function(message, contentUri) {
+ *     console.info(message + ' Go to ' + contentUri);
+ * });
+ */
+
+/**
  * Fires when a device is marked as verified/unverified/blocked/unblocked by
  * {@link module:client~MatrixClient#setDeviceVerified|MatrixClient.setDeviceVerified} or
  * {@link module:client~MatrixClient#setDeviceBlocked|MatrixClient.setDeviceBlocked}.
  *
  * @event module:client~MatrixClient#"deviceVerificationChanged"
  * @param {string} userId the owner of the verified device
  * @param {string} deviceId the id of the verified device
+ * @param {module:crypto/deviceinfo} deviceInfo updated device information
  */
 
 /**
  * Fires whenever new user-scoped account_data is added.
- * @event module:client~MatrixClient#"Room"
+ * @event module:client~MatrixClient#"accountData"
  * @param {MatrixEvent} event The event describing the account_data just added
  * @example
  * matrixClient.on("accountData", function(event){
  *   myAccountData[event.type] = event.content;
  * });
  */
 
+/**
+ * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled()
+ * @event module:client~MatrixClient#"crypto.keyBackupStatus"
+ * @param {bool} enabled true if key backup has been enabled, otherwise false
+ * @example
+ * matrixClient.on("crypto.keyBackupStatus", function(enabled){
+ *   if (enabled) {
+ *     [...]
+ *   }
+ * });
+ */
+
+/**
+ * Fires when we want to suggest to the user that they restore their megolm keys
+ * from backup or by cross-signing the device.
+ *
+ * @event module:client~MatrixClient#"crypto.suggestKeyRestore"
+ */
+
+/**
+ * Fires when a key verification is requested.
+ * @event module:client~MatrixClient#"crypto.verification.request"
+ * @param {object} data
+ * @param {MatrixEvent} data.event the original verification request message
+ * @param {Array} data.methods the verification methods that can be used
+ * @param {Function} data.beginKeyVerification a function to call if a key
+ *     verification should be performed.  The function takes one argument: the
+ *     name of the key verification method (taken from data.methods) to use.
+ * @param {Function} data.cancel a function to call if the key verification is
+ *     rejected.
+ */
+
+/**
+ * Fires when a key verification is requested with an unknown method.
+ * @event module:client~MatrixClient#"crypto.verification.request.unknown"
+ * @param {string} userId the user ID who requested the key verification
+ * @param {Function} cancel a function that will send a cancellation message to
+ *     reject the key verification.
+ */
+
+/**
+ * Fires when a key verification started message is received.
+ * @event module:client~MatrixClient#"crypto.verification.start"
+ * @param {module:crypto/verification/Base} verifier a verifier object to
+ *     perform the key verification
+ */
 
 // EventEmitter JSDocs
 
 /**
  * The {@link https://nodejs.org/api/events.html|EventEmitter} class.
  * @external EventEmitter
  * @see {@link https://nodejs.org/api/events.html}
  */
@@ -3114,19 +4677,19 @@ module.exports.CRYPTO_ENABLED = CRYPTO_E
  * The standard MatrixClient callback interface. Functions which accept this
  * will specify 2 return arguments. These arguments map to the 2 parameters
  * specified in this callback.
  * @callback module:client.callback
  * @param {Object} err The error value, the "rejected" value or null.
  * @param {Object} data The data returned, the "resolved" value.
  */
 
- /**
-  * {@link https://github.com/kriskowal/q|A promise implementation (Q)}. Functions
-  * which return this will specify 2 return arguments. These arguments map to the
-  * "onFulfilled" and "onRejected" values of the Promise.
-  * @typedef {Object} Promise
-  * @static
-  * @property {Function} then promise.then(onFulfilled, onRejected, onProgress)
-  * @property {Function} catch promise.catch(onRejected)
-  * @property {Function} finally promise.finally(callback)
-  * @property {Function} done promise.done(onFulfilled, onRejected, onProgress)
-  */
+/**
+ * {@link https://github.com/kriskowal/q|A promise implementation (Q)}. Functions
+ * which return this will specify 2 return arguments. These arguments map to the
+ * "onFulfilled" and "onRejected" values of the Promise.
+ * @typedef {Object} Promise
+ * @static
+ * @property {Function} then promise.then(onFulfilled, onRejected, onProgress)
+ * @property {Function} catch promise.catch(onRejected)
+ * @property {Function} finally promise.finally(callback)
+ * @property {Function} done promise.done(onFulfilled, onRejected, onProgress)
+ */
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/chat/protocols/matrix/lib/matrix-sdk/content-helpers.js
@@ -0,0 +1,101 @@
+/*
+Copyright 2018 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+"use strict";
+
+/** @module ContentHelpers */
+
+module.exports = {
+    /**
+     * Generates the content for a HTML Message event
+     * @param {string} body the plaintext body of the message
+     * @param {string} htmlBody the HTML representation of the message
+     * @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
+     */
+    makeHtmlMessage: function (body, htmlBody) {
+        return {
+            msgtype: "m.text",
+            format: "org.matrix.custom.html",
+            body: body,
+            formatted_body: htmlBody
+        };
+    },
+
+    /**
+     * Generates the content for a HTML Notice event
+     * @param {string} body the plaintext body of the notice
+     * @param {string} htmlBody the HTML representation of the notice
+     * @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
+     */
+    makeHtmlNotice: function (body, htmlBody) {
+        return {
+            msgtype: "m.notice",
+            format: "org.matrix.custom.html",
+            body: body,
+            formatted_body: htmlBody
+        };
+    },
+
+    /**
+     * Generates the content for a HTML Emote event
+     * @param {string} body the plaintext body of the emote
+     * @param {string} htmlBody the HTML representation of the emote
+     * @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
+     */
+    makeHtmlEmote: function (body, htmlBody) {
+        return {
+            msgtype: "m.emote",
+            format: "org.matrix.custom.html",
+            body: body,
+            formatted_body: htmlBody
+        };
+    },
+
+    /**
+     * Generates the content for a Plaintext Message event
+     * @param {string} body the plaintext body of the emote
+     * @returns {{msgtype: string, body: string}}
+     */
+    makeTextMessage: function (body) {
+        return {
+            msgtype: "m.text",
+            body: body
+        };
+    },
+
+    /**
+     * Generates the content for a Plaintext Notice event
+     * @param {string} body the plaintext body of the notice
+     * @returns {{msgtype: string, body: string}}
+     */
+    makeNotice: function (body) {
+        return {
+            msgtype: "m.notice",
+            body: body
+        };
+    },
+
+    /**
+     * Generates the content for a Plaintext Emote event
+     * @param {string} body the plaintext body of the emote
+     * @returns {{msgtype: string, body: string}}
+     */
+    makeEmoteMessage: function (body) {
+        return {
+            msgtype: "m.emote",
+            body: body
+        };
+    }
+};
\ No newline at end of file
--- a/chat/protocols/matrix/lib/matrix-sdk/content-repo.js
+++ b/chat/protocols/matrix/lib/matrix-sdk/content-repo.js
@@ -1,8 +1,10 @@
+"use strict";
+
 /*
 Copyright 2015, 2016 OpenMarket Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
     http://www.apache.org/licenses/LICENSE-2.0
@@ -11,17 +13,17 @@ Unless required by applicable law or agr
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */
 /**
  * @module content-repo
  */
-var utils = require("./utils");
+const utils = require("./utils");
 
 /** Content Repo utility functions */
 module.exports = {
     /**
      * Get the HTTP URL for an MXC URI.
      * @param {string} baseUrl The base homeserver url which has a content repo.
      * @param {string} mxc The mxc:// URI.
      * @param {Number} width The desired width of the thumbnail.
@@ -29,77 +31,77 @@ module.exports = {
      * @param {string} resizeMethod The thumbnail resize method to use, either
      * "crop" or "scale".
      * @param {Boolean} 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 the emptry string
      * for such URLs.
      * @return {string} The complete URL to the content.
      */
-    getHttpUriForMxc: function(baseUrl, mxc, width, height,
-                               resizeMethod, allowDirectLinks) {
+    getHttpUriForMxc: function (baseUrl, mxc, width, height, resizeMethod, allowDirectLinks) {
         if (typeof mxc !== "string" || !mxc) {
             return '';
         }
         if (mxc.indexOf("mxc://") !== 0) {
             if (allowDirectLinks) {
                 return mxc;
             } else {
                 return '';
             }
         }
-        var serverAndMediaId = mxc.slice(6); // strips mxc://
-        var prefix = "/_matrix/media/v1/download/";
-        var params = {};
+        let serverAndMediaId = mxc.slice(6); // strips mxc://
+        let prefix = "/_matrix/media/r0/download/";
+        const params = {};
 
         if (width) {
-            params.width = width;
+            params.width = Math.round(width);
         }
         if (height) {
-            params.height = height;
+            params.height = Math.round(height);
         }
         if (resizeMethod) {
             params.method = resizeMethod;
         }
         if (utils.keys(params).length > 0) {
             // these are thumbnailing params so they probably want the
             // thumbnailing API...
-            prefix = "/_matrix/media/v1/thumbnail/";
+            prefix = "/_matrix/media/r0/thumbnail/";
         }
 
-        var fragmentOffset = serverAndMediaId.indexOf("#"),
-            fragment = "";
+        const fragmentOffset = serverAndMediaId.indexOf("#");
+        let fragment = "";
         if (fragmentOffset >= 0) {
             fragment = serverAndMediaId.substr(fragmentOffset);
             serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
         }
-        return baseUrl + prefix + serverAndMediaId +
-            (utils.keys(params).length === 0 ? "" :
-            ("?" + utils.encodeParams(params))) + fragment;
+        return baseUrl + prefix + serverAndMediaId + (utils.keys(params).length === 0 ? "" : "?" + utils.encodeParams(params)) + fragment;
     },
 
     /**
      * Get an identicon URL from an arbitrary string.
      * @param {string} baseUrl The base homeserver url which has a content repo.
      * @param {string} identiconString The string to create an identicon for.
      * @param {Number} width The desired width of the image in pixels. Default: 96.
      * @param {Number} height The desired height of the image in pixels. Default: 96.
      * @return {string} The complete URL to the identicon.
+     * @deprecated This is no longer in the specification.
      */
-    getIdenticonUri: function(baseUrl, identiconString, width, height) {
+    getIdenticonUri: function (baseUrl, identiconString, width, height) {
         if (!identiconString) {
             return null;
         }
-        if (!width) { width = 96; }
-        if (!height) { height = 96; }
-        var params = {
+        if (!width) {
+            width = 96;
+        }
+        if (!height) {
+            height = 96;
+        }
+        const params = {
             width: width,
             height: height
         };
 
-        var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
+        const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
             $ident: identiconString
         });
-        return baseUrl + path +
-            (utils.keys(params).length === 0 ? "" :
-                ("?" + utils.encodeParams(params)));
+        return baseUrl + path + (utils.keys(params).length === 0 ? "" : "?" + utils.encodeParams(params));
     }
-};
+};
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/chat/protocols/matrix/lib/matrix-sdk/crypto/DeviceList.js
@@ -0,0 +1,875 @@
+/*
+Copyright 2017 Vector Creations Ltd
+Copyright 2018, 2019 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+"use strict";
+
+/**
+ * @module crypto/DeviceList
+ *
+ * Manages the list of other users' devices
+ */
+
+Object.defineProperty(exports, "__esModule", {
+    value: true
+});
+
+var _bluebird = require('bluebird');
+
+var _bluebird2 = _interopRequireDefault(_bluebird);
+
+var _logger = require('../logger');
+
+var _logger2 = _interopRequireDefault(_logger);
+
+var _deviceinfo = require('./deviceinfo');
+
+var _deviceinfo2 = _interopRequireDefault(_deviceinfo);
+
+var _olmlib = require('./olmlib');
+
+var _olmlib2 = _interopRequireDefault(_olmlib);
+
+var _indexeddbCryptoStore = require('./store/indexeddb-crypto-store');
+
+var _indexeddbCryptoStore2 = _interopRequireDefault(_indexeddbCryptoStore);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/* State transition diagram for DeviceList._deviceTrackingStatus
+ *
+ *                                |
+ *     stopTrackingDeviceList     V
+ *   +---------------------> NOT_TRACKED
+ *   |                            |
+ *   +<--------------------+      | startTrackingDeviceList
+ *   |                     |      V
+ *   |   +-------------> PENDING_DOWNLOAD <--------------------+-+
+ *   |   |                      ^ |                            | |
+ *   |   | restart     download | |  start download            | | invalidateUserDeviceList
+ *   |   | client        failed | |                            | |
+ *   |   |                      | V                            | |
+ *   |   +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
+ *   |                    |       |                              |
+ *   +<-------------------+       |  download successful         |
+ *   ^                            V                              |
+ *   +----------------------- UP_TO_DATE ------------------------+
+ */
+
+// constants for DeviceList._deviceTrackingStatus
+const TRACKING_STATUS_NOT_TRACKED = 0;
+const TRACKING_STATUS_PENDING_DOWNLOAD = 1;
+const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
+const TRACKING_STATUS_UP_TO_DATE = 3;
+
+/**
+ * @alias module:crypto/DeviceList
+ */
+class DeviceList {
+    constructor(baseApis, cryptoStore, olmDevice) {
+        this._cryptoStore = cryptoStore;
+
+        // userId -> {
+        //     deviceId -> {
+        //         [device info]
+        //     }
+        // }
+        this._devices = {};
+
+        // map of identity keys to the user who owns it
+        this._userByIdentityKey = {};
+
+        // which users we are tracking device status for.
+        // userId -> TRACKING_STATUS_*
+        this._deviceTrackingStatus = {}; // loaded from storage in load()
+
+        // The 'next_batch' sync token at the point the data was writen,
+        // ie. a token representing the point immediately after the
+        // moment represented by the snapshot in the db.
+        this._syncToken = null;
+
+        this._serialiser = new DeviceListUpdateSerialiser(baseApis, olmDevice, this);
+
+        // userId -> promise
+        this._keyDownloadsInProgressByUser = {};
+
+        // Set whenever changes are made other than setting the sync token
+        this._dirty = false;
+
+        // Promise resolved when device data is saved
+        this._savePromise = null;
+        // Function that resolves the save promise
+        this._resolveSavePromise = null;
+        // The time the save is scheduled for
+        this._savePromiseTime = null;
+        // The timer used to delay the save
+        this._saveTimer = null;
+    }
+
+    /**
+     * Load the device tracking state from storage
+     */
+    async load() {
+        await this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore2.default.STORE_DEVICE_DATA], txn => {
+            this._cryptoStore.getEndToEndDeviceData(txn, deviceData => {
+                this._devices = deviceData ? deviceData.devices : {}, this._deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {};
+                this._syncToken = deviceData ? deviceData.syncToken : null;
+                this._userByIdentityKey = {};
+                for (const user of Object.keys(this._devices)) {
+                    const userDevices = this._devices[user];
+                    for (const device of Object.keys(userDevices)) {
+                        const idKey = userDevices[device].keys['curve25519:' + device];
+                        if (idKey !== undefined) {
+                            this._userByIdentityKey[idKey] = user;
+                        }
+                    }
+                }
+            });
+        });
+
+        for (const u of Object.keys(this._deviceTrackingStatus)) {
+            // if a download was in progress when we got shut down, it isn't any more.
+            if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
+                this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
+            }
+        }
+    }
+
+    stop() {
+        if (this._saveTimer !== null) {
+            clearTimeout(this._saveTimer);
+        }
+    }
+
+    /**
+     * Save the device tracking state to storage, if any changes are
+     * pending other than updating the sync token
+     *
+     * The actual save will be delayed by a short amount of time to
+     * aggregate multiple writes to the database.
+     *
+     * @param {integer} delay Time in ms before which the save actually happens.
+     *     By default, the save is delayed for a short period in order to batch
+     *     multiple writes, but this behaviour can be disabled by passing 0.
+     *
+     * @return {Promise<bool>} true if the data was saved, false if
+     *     it was not (eg. because no changes were pending). The promise
+     *     will only resolve once the data is saved, so may take some time
+     *     to resolve.
+     */
+    async saveIfDirty(delay) {
+        if (!this._dirty) return _bluebird2.default.resolve(false);
+        // Delay saves for a bit so we can aggregate multiple saves that happen
+        // in quick succession (eg. when a whole room's devices are marked as known)
+        if (delay === undefined) delay = 500;
+
+        const targetTime = Date.now + delay;
+        if (this._savePromiseTime && targetTime < this._savePromiseTime) {
+            // There's a save scheduled but for after we would like: cancel
+            // it & schedule one for the time we want
+            clearTimeout(this._saveTimer);
+            this._saveTimer = null;
+            this._savePromiseTime = null;
+            // (but keep the save promise since whatever called save before
+            // will still want to know when the save is done)
+        }
+
+        let savePromise = this._savePromise;
+        if (savePromise === null) {
+            savePromise = new _bluebird2.default((resolve, reject) => {
+                this._resolveSavePromise = resolve;
+            });
+            this._savePromise = savePromise;
+        }
+
+        if (this._saveTimer === null) {
+            const resolveSavePromise = this._resolveSavePromise;
+            this._savePromiseTime = targetTime;
+            this._saveTimer = setTimeout(() => {
+                _logger2.default.log('Saving device tracking data at token ' + this._syncToken);
+                // null out savePromise now (after the delay but before the write),
+                // otherwise we could return the existing promise when the save has
+                // actually already happened. Likewise for the dirty flag.
+                this._savePromiseTime = null;
+                this._saveTimer = null;
+                this._savePromise = null;
+                this._resolveSavePromise = null;
+
+                this._dirty = false;
+                this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore2.default.STORE_DEVICE_DATA], txn => {
+                    this._cryptoStore.storeEndToEndDeviceData({
+                        devices: this._devices,
+                        trackingStatus: this._deviceTrackingStatus,
+                        syncToken: this._syncToken
+                    }, txn);
+                }).then(() => {
+                    resolveSavePromise();
+                });
+            }, delay);
+        }
+        return savePromise;
+    }
+
+    /**
+     * Gets the sync token last set with setSyncToken
+     *
+     * @return {string} The sync token
+     */
+    getSyncToken() {
+        return this._syncToken;
+    }
+
+    /**
+     * Sets the sync token that the app will pass as the 'since' to the /sync
+     * endpoint next time it syncs.
+     * The sync token must always be set after any changes made as a result of
+     * data in that sync since setting the sync token to a newer one will mean
+     * those changed will not be synced from the server if a new client starts
+     * up with that data.
+     *
+     * @param {string} st The sync token
+     */
+    setSyncToken(st) {
+        this._syncToken = st;
+    }
+
+    /**
+     * Ensures up to date keys for a list of users are stored in the session store,
+     * downloading and storing them if they're not (or if forceDownload is
+     * true).
+     * @param {Array} userIds The users to fetch.
+     * @param {bool} forceDownload Always download the keys even if cached.
+     *
+     * @return {Promise} A promise which resolves to a map userId->deviceId->{@link
+     * module:crypto/deviceinfo|DeviceInfo}.
+     */
+    downloadKeys(userIds, forceDownload) {
+        const usersToDownload = [];
+        const promises = [];
+
+        userIds.forEach(u => {
+            const trackingStatus = this._deviceTrackingStatus[u];
+            if (this._keyDownloadsInProgressByUser[u]) {
+                // already a key download in progress/queued for this user; its results
+                // will be good enough for us.
+                _logger2.default.log(`downloadKeys: already have a download in progress for ` + `${u}: awaiting its result`);
+                promises.push(this._keyDownloadsInProgressByUser[u]);
+            } else if (forceDownload || trackingStatus != TRACKING_STATUS_UP_TO_DATE) {
+                usersToDownload.push(u);
+            }
+        });
+
+        if (usersToDownload.length != 0) {
+            _logger2.default.log("downloadKeys: downloading for", usersToDownload);
+            const downloadPromise = this._doKeyDownload(usersToDownload);
+            promises.push(downloadPromise);
+        }
+
+        if (promises.length === 0) {
+            _logger2.default.log("downloadKeys: already have all necessary keys");
+        }
+
+        return _bluebird2.default.all(promises).then(() => {
+            return this._getDevicesFromStore(userIds);
+        });
+    }
+
+    /**
+     * Get the stored device keys for a list of user ids
+     *
+     * @param {string[]} userIds the list of users to list keys for.
+     *
+     * @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}.
+     */
+    _getDevicesFromStore(userIds) {
+        const stored = {};
+        const self = this;
+        userIds.map(function (u) {
+            stored[u] = {};
+            const devices = self.getStoredDevicesForUser(u) || [];
+            devices.map(function (dev) {
+                stored[u][dev.deviceId] = dev;
+            });
+        });
+        return stored;
+    }
+
+    /**
+     * Get the stored device keys for a user id
+     *
+     * @param {string} userId the user to list keys for.
+     *
+     * @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't
+     * managed to get a list of devices for this user yet.
+     */
+    getStoredDevicesForUser(userId) {
+        const devs = this._devices[userId];
+        if (!devs) {
+            return null;
+        }
+        const res = [];
+        for (const deviceId in devs) {
+            if (devs.hasOwnProperty(deviceId)) {
+                res.push(_deviceinfo2.default.fromStorage(devs[deviceId], deviceId));
+            }
+        }
+        return res;
+    }
+
+    /**
+     * Get the stored device dat