From ebf579d7b0ac85f33b2c8fbc851487825696eb9c Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Mon, 3 Aug 2020 22:25:16 +0200
Subject: [PATCH 01/30] Convert intent.js to TypeScript

---
 src/components/intent.js | 827 -------------------------------------
 src/components/intent.ts | 856 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 856 insertions(+), 827 deletions(-)
 delete mode 100644 src/components/intent.js
 create mode 100644 src/components/intent.ts

diff --git a/src/components/intent.js b/src/components/intent.js
deleted file mode 100644
index 91d7f4f8..00000000
--- a/src/components/intent.js
+++ /dev/null
@@ -1,827 +0,0 @@
-/*
-Copyright 2020 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.
-*/
-
-const Promise = require("bluebird");
-const MatrixUser = require("../models/users/matrix");
-const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
-const RoomMember = require("matrix-js-sdk").RoomMember;
-const ClientRequestCache = require("./client-request-cache");
-
-const STATE_EVENT_TYPES = [
-    "m.room.name", "m.room.topic", "m.room.power_levels", "m.room.member",
-    "m.room.join_rules", "m.room.history_visibility"
-];
-const DEFAULT_CACHE_TTL = 90000;
-const DEFAULT_CACHE_SIZE = 1024;
-
-/**
- * Create an entity which can fulfil the intent of a given user.
- * @constructor
- * @param {MatrixClient} client The matrix client instance whose intent is being
- * fulfilled e.g. the entity joining the room when you call intent.join(roomId).
- * @param {MatrixClient} botClient The client instance for the AS bot itself.
- * This will be used to perform more priveleged actions such as creating new
- * rooms, sending invites, etc.
- * @param {Object} opts Options for this Intent instance.
- * @param {boolean} opts.registered True to inform this instance that the client
- * is already registered. No registration requests will be made from this Intent.
- * Default: false.
- * @param {boolean} opts.dontCheckPowerLevel True to not check for the right power
- * level before sending events. Default: false.
- *
- * @param {Object=} opts.backingStore An object with 4 functions, outlined below.
- * If this Object is supplied, ALL 4 functions must be supplied. If this Object
- * is not supplied, the Intent will maintain its own backing store for membership
- * and power levels, which may scale badly for lots of users.
- *
- * @param {Function} opts.backingStore.getMembership A function which is called with a
- * room ID and user ID which should return the membership status of this user as
- * a string e.g "join". `null` should be returned if the membership is unknown.
- *
- * @param {Function} opts.backingStore.getPowerLevelContent A function which is called
- * with a room ID which should return the power level content for this room, as an Object.
- * `null` should be returned if there is no known content.
- *
- * @param {Function} opts.backingStore.setMembership A function with the signature:
- * function(roomId, userId, membership) which will set the membership of the given user in
- * the given room. This has no return value.
- *
- * @param {Function} opts.backingStore.setPowerLevelContent A function with the signature:
- * function(roomId, content) which will set the power level content in the given room.
- * This has no return value.
- *
- * @param {boolean} opts.dontJoin True to not attempt to join a room before
- * sending messages into it. The surrounding code will have to ensure the correct
- * membership state itself in this case. Default: false.
- *
- * @param {boolean} [opts.enablePresence=true] True to send presence, false to no-op.
- *
- * @param {Number} opts.caching.ttl How long requests can stay in the cache, in milliseconds.
- * @param {Number} opts.caching.size How many entries should be kept in the cache, before the oldest is dropped.
- */
-function Intent(client, botClient, opts) {
-    this.client = client;
-    this.botClient = botClient;
-    opts = opts || {};
-
-    opts.enablePresence = opts.enablePresence !== false;
-
-    if (opts.backingStore) {
-        if (!opts.backingStore.setPowerLevelContent ||
-                !opts.backingStore.getPowerLevelContent ||
-                !opts.backingStore.setMembership ||
-                !opts.backingStore.getMembership) {
-            throw new Error("Intent backingStore missing required functions");
-        }
-    }
-    else {
-        this._membershipStates = {
-        //  room_id : "join|invite|leave|null"   null=unknown
-        };
-        this._powerLevels = {
-        //  room_id: event.content
-        };
-        var self = this;
-
-        opts.backingStore = {
-            getMembership: function(roomId, userId) {
-                if (userId !== self.client.credentials.userId) {
-                    return null;
-                }
-                return self._membershipStates[roomId];
-            },
-            setMembership: function(roomId, userId, membership) {
-                if (userId !== self.client.credentials.userId) {
-                    return;
-                }
-                self._membershipStates[roomId] = membership;
-            },
-            setPowerLevelContent: function(roomId, content) {
-                self._powerLevels[roomId] = content;
-            },
-            getPowerLevelContent: function(roomId) {
-                return self._powerLevels[roomId];
-            }
-        }
-    }
-
-    if (!opts.caching) {
-        opts.caching = { };
-    }
-
-    opts.caching.ttl = opts.caching.ttl === undefined ? DEFAULT_CACHE_TTL : opts.caching.ttl;
-    opts.caching.size = opts.caching.size === undefined ? DEFAULT_CACHE_SIZE : opts.caching.ttl;
-    this._requestCaches = {};
-    this._requestCaches.profile = new ClientRequestCache(
-        opts.caching.ttl,
-        opts.caching.size,
-        (_, userId, info) => {
-            return this.getProfileInfo(userId, info, false);
-        }
-    );
-    this._requestCaches.roomstate = new ClientRequestCache(
-        opts.caching.ttl,
-        opts.caching.size,
-        (roomId) => {
-        return this.roomState(roomId, false);
-        }
-    );
-    this._requestCaches.event = new ClientRequestCache(
-        opts.caching.ttl,
-        opts.caching.size,
-        (_, roomId, eventId) => {
-        return this.getEvent(roomId, eventId, false);
-        }
-    );
-
-    this.opts = opts;
-}
-
-/**
- * Return the client this Intent is acting on behalf of.
- * @return {MatrixClient} The client
- */
-Intent.prototype.getClient = function() {
-    return this.client;
-};
-
-/**
- * <p>Send a plaintext message to a room.</p>
- * This will automatically make the client join the room so they can send the
- * message if they are not already joined. It will also make sure that the client
- * has sufficient power level to do this.
- * @param {string} roomId The room to send to.
- * @param {string} text The text string to send.
- * @return {Promise}
- */
-Intent.prototype.sendText = function(roomId, text) {
-    return this.sendMessage(roomId, {
-        body: text,
-        msgtype: "m.text"
-    });
-};
-
-/**
- * <p>Set the name of a room.</p>
- * This will automatically make the client join the room so they can set the
- * name if they are not already joined. It will also make sure that the client
- * has sufficient power level to do this.
- * @param {string} roomId The room to send to.
- * @param {string} name The room name.
- * @return {Promise}
- */
-Intent.prototype.setRoomName = function(roomId, name) {
-    return this.sendStateEvent(roomId, "m.room.name", "", {
-        name: name
-    });
-};
-
-/**
- * <p>Set the topic of a room.</p>
- * This will automatically make the client join the room so they can set the
- * topic if they are not already joined. It will also make sure that the client
- * has sufficient power level to do this.
- * @param {string} roomId The room to send to.
- * @param {string} topic The room topic.
- * @return {Promise}
- */
-Intent.prototype.setRoomTopic = function(roomId, topic) {
-    return this.sendStateEvent(roomId, "m.room.topic", "", {
-        topic: topic
-    });
-};
-
-/**
- * <p>Set the avatar of a room.</p>
- * This will automatically make the client join the room so they can set the
- * topic if they are not already joined. It will also make sure that the client
- * has sufficient power level to do this.
- * @param {string} roomId The room to send to.
- * @param {string} avatar The url of the avatar.
- * @param {string} info Extra information about the image. See m.room.avatar for details.
- * @return {Promise}
- */
-Intent.prototype.setRoomAvatar = function(roomId, avatar, info) {
-    var content = {
-        url: avatar
-    };
-    if (info) {
-        content.info = info;
-    }
-    return this.sendStateEvent(roomId, "m.room.avatar", "", content);
-};
-
-/**
- * <p>Send a typing event to a room.</p>
- * This will automatically make the client join the room so they can send the
- * typing event if they are not already joined.
- * @param {string} roomId The room to send to.
- * @param {boolean} isTyping True if typing
- * @return {Promise}
- */
-Intent.prototype.sendTyping = function(roomId, isTyping) {
-    var self = this;
-    return self._ensureJoined(roomId).then(function() {
-        return self._ensureHasPowerLevelFor(roomId, "m.typing");
-    }).then(function() {
-        return self.client.sendTyping(roomId, isTyping);
-    });
-};
-
-/**
- * <p>Send a read receipt to a room.</p>
- * This will automatically make the client join the room so they can send the
- * receipt event if they are not already joined.
- * @param{string} roomId The room to send to.
- * @param{string} eventId The event ID to set the receipt mark to.
- * @return {Promise}
- */
-Intent.prototype.sendReadReceipt = function(roomId, eventId) {
-    var self = this;
-    var event = new MatrixEvent({
-        room_id: roomId,
-        event_id: eventId,
-    });
-    return self._ensureJoined(roomId).then(function() {
-        return self.client.sendReadReceipt(event);
-    });
-}
-
-/**
- * Set the power level of the given target.
- * @param {string} roomId The room to set the power level in.
- * @param {string} target The target user ID
- * @param {number} level The desired level
- * @return {Promise}
- */
-Intent.prototype.setPowerLevel = function(roomId, target, level) {
-    var self = this;
-    return self._ensureJoined(roomId).then(function() {
-        return self._ensureHasPowerLevelFor(roomId, "m.room.power_levels");
-    }).then(function(event) {
-        return self.client.setPowerLevel(roomId, target, level, event);
-    });
-};
-
-/**
- * <p>Send an <code>m.room.message</code> event to a room.</p>
- * This will automatically make the client join the room so they can send the
- * message if they are not already joined. It will also make sure that the client
- * has sufficient power level to do this.
- * @param {string} roomId The room to send to.
- * @param {Object} content The event content
- * @return {Promise}
- */
-Intent.prototype.sendMessage = function(roomId, content) {
-    return this.sendEvent(roomId, "m.room.message", content);
-};
-
-/**
- * <p>Send a message event to a room.</p>
- * This will automatically make the client join the room so they can send the
- * message if they are not already joined. It will also make sure that the client
- * has sufficient power level to do this.
- * @param {string} roomId The room to send to.
- * @param {string} type The event type
- * @param {Object} content The event content
- * @return {Promise}
- */
-Intent.prototype.sendEvent = function(roomId, type, content) {
-    var self = this;
-    return self._ensureJoined(roomId).then(function() {
-        return self._ensureHasPowerLevelFor(roomId, type);
-    }).then(self._joinGuard(roomId, function() {
-        return self.client.sendEvent(roomId, type, content);
-    }));
-};
-
-/**
- * <p>Send a state event to a room.</p>
- * This will automatically make the client join the room so they can send the
- * state if they are not already joined. It will also make sure that the client
- * has sufficient power level to do this.
- * @param {string} roomId The room to send to.
- * @param {string} type The event type
- * @param {string} skey The state key
- * @param {Object} content The event content
- * @return {Promise}
- */
-Intent.prototype.sendStateEvent = function(roomId, type, skey, content) {
-    var self = this;
-    return self._ensureJoined(roomId).then(function() {
-        return self._ensureHasPowerLevelFor(roomId, type);
-    }).then(self._joinGuard(roomId, function() {
-        return self.client.sendStateEvent(roomId, type, content, skey);
-    }));
-};
-
-/**
- * <p>Get the current room state for a room.</p>
- * This will automatically make the client join the room so they can get the
- * state if they are not already joined.
- * @param {string} roomId The room to get the state from.
- * @param {boolean} [useCache=false] Should the request attempt to lookup
- * state from the cache.
- * @return {Promise}
- */
-Intent.prototype.roomState = function(roomId, useCache=false) {
-    return this._ensureJoined(roomId).then(() => {
-        if (useCache) {
-            return this._requestCaches.roomstate.get(roomId);
-        }
-        return this.client.roomState(roomId);
-    });
-};
-
-/**
- * Create a room with a set of options.
- * @param {Object} opts Options.
- * @param {boolean} opts.createAsClient True to create this room as a client and
- * not the bot: the bot will not join. False to create this room as the bot and
- * auto-join the client. Default: false.
- * @param {Object} opts.options Options to pass to the client SDK /createRoom API.
- * @return {Promise}
- */
-Intent.prototype.createRoom = function(opts) {
-    var self = this;
-    var cli = opts.createAsClient ? this.client : this.botClient;
-    var options = opts.options || {};
-    if (!opts.createAsClient) {
-        // invite the client if they aren't already
-        options.invite = options.invite || [];
-        if (options.invite.indexOf(this.client.credentials.userId) === -1) {
-            options.invite.push(this.client.credentials.userId);
-        }
-    }
-    // make sure that the thing doing the room creation isn't inviting itself
-    // else Synapse hard fails the operation with M_FORBIDDEN
-    if (options.invite && options.invite.indexOf(cli.credentials.userId) !== -1) {
-        options.invite.splice(options.invite.indexOf(cli.credentials.userId), 1);
-    }
-
-    return this._ensureRegistered().then(function() {
-        return cli.createRoom(options);
-    }).then(function(res) {
-        // create a fake power level event to give the room creator ops if we
-        // don't yet have a power level event.
-        if (self.opts.backingStore.getPowerLevelContent(res.room_id)) {
-            return res;
-        }
-        var users = {};
-        users[cli.credentials.userId] = 100;
-        self.opts.backingStore.setPowerLevelContent(res.room_id, {
-            users_default: 0,
-            events_default: 0,
-            state_default: 50,
-            users: users,
-            events: {}
-        });
-        return res;
-    });
-};
-
-/**
- * <p>Invite a user to a room.</p>
- * This will automatically make the client join the room so they can send the
- * invite if they are not already joined.
- * @param {string} roomId The room to invite the user to.
- * @param {string} target The user ID to invite.
- * @return {Promise} Resolved when invited, else rejected with an error.
- */
-Intent.prototype.invite = function(roomId, target) {
-    var self = this;
-    return this._ensureJoined(roomId).then(function() {
-        return self.client.invite(roomId, target);
-    });
-};
-
-/**
- * <p>Kick a user from a room.</p>
- * This will automatically make the client join the room so they can send the
- * kick if they are not already joined.
- * @param {string} roomId The room to kick the user from.
- * @param {string} target The target of the kick operation.
- * @param {string} reason Optional. The reason for the kick.
- * @return {Promise} Resolved when kickked, else rejected with an error.
- */
-Intent.prototype.kick = function(roomId, target, reason) {
-    var self = this;
-    return this._ensureJoined(roomId).then(function() {
-        return self.client.kick(roomId, target, reason);
-    });
-};
-
-/**
- * <p>Ban a user from a room.</p>
- * This will automatically make the client join the room so they can send the
- * ban if they are not already joined.
- * @param {string} roomId The room to ban the user from.
- * @param {string} target The target of the ban operation.
- * @param {string} reason Optional. The reason for the ban.
- * @return {Promise} Resolved when banned, else rejected with an error.
- */
-Intent.prototype.ban = function(roomId, target, reason) {
-    var self = this;
-    return this._ensureJoined(roomId).then(function() {
-        return self.client.ban(roomId, target, reason);
-    });
-};
-
-/**
- * <p>Unban a user from a room.</p>
- * This will automatically make the client join the room so they can send the
- * unban if they are not already joined.
- * @param {string} roomId The room to unban the user from.
- * @param {string} target The target of the unban operation.
- * @return {Promise} Resolved when unbanned, else rejected with an error.
- */
-Intent.prototype.unban = function(roomId, target) {
-    var self = this;
-    return this._ensureJoined(roomId).then(function() {
-        return self.client.unban(roomId, target);
-    });
-};
-
-/**
- * <p>Join a room</p>
- * This will automatically send an invite from the bot if it is an invite-only
- * room, which may make the bot attempt to join the room if it isn't already.
- * @param {string} roomId The room to join.
- * @param {string[]} viaServers The server names to try and join through in
- * addition to those that are automatically chosen.
- * @return {Promise}
- */
-Intent.prototype.join = function(roomId, viaServers) {
-    return this._ensureJoined(roomId, false, viaServers);
-};
-
-/**
- * <p>Leave a room</p>
- * This will no-op if the user isn't in the room.
- * @param {string} roomId The room to leave.
- * @return {Promise}
- */
-Intent.prototype.leave = function(roomId) {
-    return this.client.leave(roomId);
-};
-
-/**
- * <p>Get a user's profile information</p>
- * @param {string} userId The ID of the user whose profile to return
- * @param {string} info The profile field name to retrieve (e.g. 'displayname'
- * or 'avatar_url'), or null to fetch the entire profile information.
- * @param {boolean} [useCache=true] Should the request attempt to lookup
- * state from the cache.
- * @return {Promise} A Promise that resolves with the requested user's profile
- * information
- */
-Intent.prototype.getProfileInfo = function(userId, info, useCache=true) {
-    return this._ensureRegistered().then(() => {
-        if (useCache) {
-            return this._requestCaches.profile.get(`${userId}:${info}`, userId, info);
-        }
-        return this.client.getProfileInfo(userId, info);
-    });
-};
-
-/**
- * <p>Set the user's display name</p>
- * @param {string} name The new display name
- * @return {Promise}
- */
-Intent.prototype.setDisplayName = function(name) {
-    var self = this;
-    return self._ensureRegistered().then(function() {
-        return self.client.setDisplayName(name);
-    });
-};
-
-/**
- * <p>Set the user's avatar URL</p>
- * @param {string} url The new avatar URL
- * @return {Promise}
- */
-Intent.prototype.setAvatarUrl = function(url) {
-    var self = this;
-    return self._ensureRegistered().then(function() {
-        return self.client.setAvatarUrl(url);
-    });
-};
-
-/**
- * Create a new alias mapping.
- * @param {string} alias The room alias to create
- * @param {string} roomId The room ID the alias should point at.
- * @return {Promise}
- */
-Intent.prototype.createAlias = function(alias, roomId) {
-    var self = this;
-    return self._ensureRegistered().then(function() {
-        return self.client.createAlias(alias, roomId);
-    });
-};
-
-/**
- * Set the presence of this user.
- * @param {string} presence One of "online", "offline" or "unavailable".
- * @param {string} status_msg The status message to attach.
- * @return {Promise} Resolves if the presence was set or no-oped, rejects otherwise.
- */
-Intent.prototype.setPresence = function(presence, status_msg=undefined) {
-    if (!this.opts.enablePresence) {
-        return Promise.resolve();
-    }
-
-    return this._ensureRegistered().then(() => {
-        return this.client.setPresence({presence, status_msg});
-    });
-};
-
-/**
- * @typedef {
- *       "m.event_not_handled"
- *     | "m.event_too_old"
- *     | "m.internal_error"
- *     | "m.foreign_network_error"
- *     | "m.event_unknown"
- * } BridgeErrorReason
- */
-
-/**
- * Signals that an error occured while handling an event by the bridge.
- *
- * **Warning**: This function is unstable and is likely to change pending the outcome
- * of https://github.com/matrix-org/matrix-doc/pull/2162.
- * @param {string} roomID ID of the room in which the error occured.
- * @param {string} eventID ID of the event for which the error occured.
- * @param {string} networkName Name of the bridged network.
- * @param {BridgeErrorReason} reason The reason why the bridge error occured.
- * @param {string} reason_body A human readable string d
- * @param {string[]} affectedUsers Array of regex matching all affected users.
- * @return {Promise}
- */
-Intent.prototype.unstableSignalBridgeError = function(
-    roomID,
-    eventID,
-    networkName,
-    reason,
-    affectedUsers
-) {
-    return this.sendEvent(
-        roomID,
-        "de.nasnotfound.bridge_error",
-        {
-            network_name: networkName,
-            reason: reason,
-            affected_users: affectedUsers,
-            "m.relates_to": {
-                rel_type: "m.reference",
-                event_id: eventID,
-            },
-        }
-    );
-}
-
-/**
- * Get an event in a room.
- * This will automatically make the client join the room so they can get the
- * event if they are not already joined.
- * @param {string} roomId The room to fetch the event from.
- * @param {string} eventId The eventId of the event to fetch.
- * @param {boolean} [useCache=true] Should the request attempt to lookup from the cache.
- * @return {Promise} Resolves with the content of the event, or rejects if not found.
- */
-Intent.prototype.getEvent = function(roomId, eventId, useCache=true) {
-    return this._ensureRegistered().then(() => {
-        if (useCache) {
-            return this._requestCaches.event.get(`${roomId}:${eventId}`, roomId, eventId);
-        }
-        return this.client.fetchRoomEvent(roomId, eventId);
-    });
-};
-
-/**
- * Get a state event in a room.
- * This will automatically make the client join the room so they can get the
- * state if they are not already joined.
- * @param {string} roomId The room to get the state from.
- * @param {string} eventType The event type to fetch.
- * @param {string} [stateKey=""] The state key of the event to fetch.
- * @return {Promise}
- */
-Intent.prototype.getStateEvent = function(roomId, eventType, stateKey = "") {
-    return this._ensureJoined(roomId).then(() => {
-        return this.client.getStateEvent(roomId, eventType, stateKey);
-    });
-};
-
-/**
- * Inform this Intent class of an incoming event. Various optimisations will be
- * done if this is provided. For example, a /join request won't be sent out if
- * it knows you've already been joined to the room. This function does nothing
- * if a backing store was provided to the Intent.
- * @param {Object} event The incoming event JSON
- */
-Intent.prototype.onEvent = function(event) {
-    if (!this._membershipStates || !this._powerLevels) {
-        return;
-    }
-    if (event.type === "m.room.member" &&
-            event.state_key === this.client.credentials.userId) {
-        this._membershipStates[event.room_id] = event.content.membership;
-    }
-    else if (event.type === "m.room.power_levels") {
-        this._powerLevels[event.room_id] = event.content;
-    }
-};
-
-// Guard a function which returns a promise which may reject if the user is not
-// in the room. If the promise rejects, join the room and retry the function.
-Intent.prototype._joinGuard = function(roomId, promiseFn) {
-    var self = this;
-    return function() {
-        return promiseFn().catch(function(err) {
-            if (err.errcode !== "M_FORBIDDEN") {
-                // not a guardable error
-                throw err;
-            }
-            return self._ensureJoined(roomId, true).then(function() {
-                return promiseFn();
-            })
-        });
-    };
-};
-
-Intent.prototype._ensureJoined = async function(
-    roomId, ignoreCache = false, viaServers = undefined, passthroughError = false
-) {
-    const userId = this.client.credentials.userId;
-    const opts = {
-        syncRoom: false,
-    };
-    if (viaServers) {
-        opts.viaServers = viaServers;
-    }
-    if (this.opts.backingStore.getMembership(roomId, userId) === "join" && !ignoreCache) {
-        return Promise.resolve();
-    }
-
-    /* Logic:
-    if client /join:
-      SUCCESS
-    else if bot /invite client:
-      if client /join:
-        SUCCESS
-      else:
-        FAIL (client couldn't join)
-    else if bot /join:
-      if bot /invite client and client /join:
-        SUCCESS
-      else:
-        FAIL (bot couldn't invite)
-    else:
-      FAIL (bot can't get into the room)
-    */
-
-    const deferredPromise = new Promise.defer();
-
-    const mark = (r, state) => {
-        this.opts.backingStore.setMembership(r, userId, state);
-        if (state === "join") {
-            deferredPromise.resolve();
-        }
-    }
-
-    const dontJoin = this.opts.dontJoin;
-
-    try {
-        await this._ensureRegistered();
-        if (dontJoin) {
-            deferredPromise.resolve();
-            return deferredPromise.promise;
-        }
-        try {
-            await this.client.joinRoom(roomId, opts);
-            mark(roomId, "join");
-        }
-        catch (ex) {
-            if (ex.errcode !== "M_FORBIDDEN" || this.botClient === this) {
-                throw ex;
-            }
-            try {
-                // Try bot inviting client
-                await this.botClient.invite(roomId, userId);
-                await this.client.joinRoom(roomId, opts);
-                mark(roomId, "join");
-            }
-            catch (_ex) {
-                // Try bot joining
-                await this.botClient.joinRoom(roomId, opts)
-                await this.botClient.invite(roomId, userId);
-                await this.client.joinRoom(roomId, opts);
-                mark(roomId, "join");
-            }
-        }
-    }
-    catch (ex) {
-        deferredPromise.reject(passthroughError ? ex : Error("Failed to join room"));
-    }
-
-    return deferredPromise.promise;
-};
-
-Intent.prototype._ensureHasPowerLevelFor = function(roomId, eventType) {
-    if (this.opts.dontCheckPowerLevel && eventType !== "m.room.power_levels") {
-        return Promise.resolve();
-    }
-    var self = this;
-    var userId = this.client.credentials.userId;
-    var plContent = this.opts.backingStore.getPowerLevelContent(roomId);
-    var promise = Promise.resolve(plContent);
-    if (!plContent) {
-        promise = this.client.getStateEvent(roomId, "m.room.power_levels", "");
-    }
-    return promise.then(function(eventContent) {
-        self.opts.backingStore.setPowerLevelContent(roomId, eventContent);
-        const event = {
-            content: eventContent,
-            room_id: roomId,
-            sender: "",
-            event_id: "_",
-            state_key: "",
-            type: "m.room.power_levels"
-        }
-        var powerLevelEvent = new MatrixEvent(event);
-        // What level do we need for this event type?
-        var defaultLevel = event.content.events_default;
-        if (STATE_EVENT_TYPES.indexOf(eventType) !== -1) {
-            defaultLevel = event.content.state_default;
-        }
-        var requiredLevel = event.content.events[eventType] || defaultLevel;
-
-        // Parse out what level the client has by abusing the JS SDK
-        var roomMember = new RoomMember(roomId, userId);
-        roomMember.setPowerLevelEvent(powerLevelEvent);
-
-        if (requiredLevel > roomMember.powerLevel) {
-            // can the bot update our power level?
-            var bot = new RoomMember(roomId, self.botClient.credentials.userId);
-            bot.setPowerLevelEvent(powerLevelEvent);
-            var levelRequiredToModifyPowerLevels = event.content.events[
-                "m.room.power_levels"
-            ] || event.content.state_default;
-            if (levelRequiredToModifyPowerLevels > bot.powerLevel) {
-                // even the bot has no power here.. give up.
-                throw new Error(
-                    "Cannot ensure client has power level for event " + eventType +
-                    " : client has " + roomMember.powerLevel + " and we require " +
-                    requiredLevel + " and the bot doesn't have permission to " +
-                    "edit the client's power level."
-                );
-            }
-            // update the client's power level first
-            return self.botClient.setPowerLevel(
-                roomId, userId, requiredLevel, powerLevelEvent
-            ).then(function() {
-                // tweak the level for the client to reflect the new reality
-                var userLevels = powerLevelEvent.getContent().users || {};
-                userLevels[userId] = requiredLevel;
-                powerLevelEvent.getContent().users = userLevels;
-                return Promise.resolve(powerLevelEvent);
-            });
-        }
-        return Promise.resolve(powerLevelEvent);
-    });
-};
-
-Intent.prototype._ensureRegistered = function() {
-    if (this.opts.registered) {
-        return Promise.resolve("registered=true");
-    }
-    const userId = this.client.credentials.userId;
-    const localpart = new MatrixUser(userId).localpart;
-    return this.botClient.register(localpart).then((res) => {
-        this.opts.registered = true;
-        return res;
-    }).catch((err) => {
-        if (err.errcode === "M_USER_IN_USE") {
-            this.opts.registered = true;
-            return null;
-        }
-        throw err;
-    });
-};
-
-module.exports = Intent;
diff --git a/src/components/intent.ts b/src/components/intent.ts
new file mode 100644
index 00000000..e1827d0b
--- /dev/null
+++ b/src/components/intent.ts
@@ -0,0 +1,856 @@
+/*
+Copyright 2020 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.
+*/
+
+import Bluebird from "bluebird";
+import MatrixUser from "../models/users/matrix";
+import JsSdk from "matrix-js-sdk";
+const { MatrixEvent, RoomMember } = JsSdk as any;
+import ClientRequestCache from "./client-request-cache";
+
+type MatrixClient = {
+    credentials: {
+        userId: string;
+    },
+    ban: (roomId: string, target: string, reason: string) => Promise<any>;
+    createAlias: (alias: string, roomId: string) => Promise<any>;
+    createRoom: (opts: Record<string,any>) => Promise<any>;
+    fetchRoomEvent: (roomId: string, eventId: string) => Promise<any>;
+    getStateEvent: (roomId: string, eventType: string, stateKey: string) => Promise<any>;
+    invite: (roomId: string, userId: string) => Promise<any>;
+    joinRoom: (roomId: string, opts: Record<string,any>) => Promise<any>;
+    kick: (roomId: string, target: string, reason: string) => Promise<any>;
+    leave: (roomId: string) => Promise<any>;
+    register: (localpart: string) => Promise<any>;
+    roomState: (roomId: string) => Promise<any>;
+    sendEvent: (roomId: string, type: string, content: Record<string,any>) => Promise<any>;
+    sendReadReceipt: (event: any) => Promise<any>;
+    sendStateEvent: (roomId: string, type: string, content: Record<string, any>, skey: string) => Promise<any>;
+    sendTyping: (roomId: string, isTyping: boolean) => Promise<any>;
+    setAvatarUrl: (url: string) => Promise<any>;
+    setDisplayName: (name: string) => Promise<any>;
+    setPowerLevel: (roomId: string, target: string, level: number, event: any) => Promise<any>;
+    setPresence: (presence: { presence: string, status_msg?: string }) => Promise<any>;
+    getProfileInfo: (userId: string, info: string) => Promise<any>;
+    unban: (roomId: string, target: string) => Promise<any>;
+};
+
+type BridgeErrorReason = "m.event_not_handled" | "m.event_too_old" | "m.internal_error" | "m.foreign_network_error" | "m.event_unknown";
+
+type MembershipState = "join" | "invite" | "leave" | null; // null = unknown
+
+interface IntentOpts {
+    backingStore?: {
+        getMembership: (roomId: string, userId: string) => MembershipState,
+        getPowerLevelContent: (roomId: string) => Record<string, any>,
+        setMembership: (roomId: string, userId: string, membership: MembershipState) => void,
+        setPowerLevelContent: (roomId: string, content: Record<string, any>) => void,
+    },
+    caching?: {
+        ttl?: number,
+        size?: number,
+    };
+    dontCheckPowerLevel?: boolean;
+    dontJoin?: boolean;
+    enablePresence?: boolean;
+    registered?: boolean;
+};
+
+const STATE_EVENT_TYPES = [
+    "m.room.name", "m.room.topic", "m.room.power_levels", "m.room.member",
+    "m.room.join_rules", "m.room.history_visibility"
+];
+const DEFAULT_CACHE_TTL = 90000;
+const DEFAULT_CACHE_SIZE = 1024;
+
+class Intent {
+    private _requestCaches: {
+        profile: ClientRequestCache,
+        roomstate: ClientRequestCache,
+        event: ClientRequestCache
+    };
+    private opts: {
+        backingStore: {
+            getMembership: (roomId: string, userId: string) => MembershipState,
+            getPowerLevelContent: (roomId: string) => Record<string,any>,
+            setMembership: (roomId: string, userId: string, membership: MembershipState) => void,
+            setPowerLevelContent: (roomId: string, content: Record<string,any>) => void,
+        },
+        caching: {
+            ttl: number,
+            size: number,
+        };
+        dontCheckPowerLevel?: boolean;
+        dontJoin?: boolean;
+        enablePresence: boolean;
+        registered?: boolean;
+    };
+    private _membershipStates?: Record<string,MembershipState>;
+    private _powerLevels?: Record<string,Record<string,any>>;
+    
+    /**
+    * Create an entity which can fulfil the intent of a given user.
+    * @constructor
+    * @param {MatrixClient} client The matrix client instance whose intent is being
+    * fulfilled e.g. the entity joining the room when you call intent.join(roomId).
+    * @param {MatrixClient} botClient The client instance for the AS bot itself.
+    * This will be used to perform more priveleged actions such as creating new
+    * rooms, sending invites, etc.
+    * @param {Object} opts Options for this Intent instance.
+    * @param {boolean} opts.registered True to inform this instance that the client
+    * is already registered. No registration requests will be made from this Intent.
+    * Default: false.
+    * @param {boolean} opts.dontCheckPowerLevel True to not check for the right power
+    * level before sending events. Default: false.
+    *
+    * @param {Object=} opts.backingStore An object with 4 functions, outlined below.
+    * If this Object is supplied, ALL 4 functions must be supplied. If this Object
+    * is not supplied, the Intent will maintain its own backing store for membership
+    * and power levels, which may scale badly for lots of users.
+    *
+    * @param {Function} opts.backingStore.getMembership A function which is called with a
+    * room ID and user ID which should return the membership status of this user as
+    * a string e.g "join". `null` should be returned if the membership is unknown.
+    *
+    * @param {Function} opts.backingStore.getPowerLevelContent A function which is called
+    * with a room ID which should return the power level content for this room, as an Object.
+    * `null` should be returned if there is no known content.
+    *
+    * @param {Function} opts.backingStore.setMembership A function with the signature:
+    * function(roomId, userId, membership) which will set the membership of the given user in
+    * the given room. This has no return value.
+    *
+    * @param {Function} opts.backingStore.setPowerLevelContent A function with the signature:
+    * function(roomId, content) which will set the power level content in the given room.
+    * This has no return value.
+    *
+    * @param {boolean} opts.dontJoin True to not attempt to join a room before
+    * sending messages into it. The surrounding code will have to ensure the correct
+    * membership state itself in this case. Default: false.
+    *
+    * @param {boolean} [opts.enablePresence=true] True to send presence, false to no-op.
+    *
+    * @param {Number} opts.caching.ttl How long requests can stay in the cache, in milliseconds.
+    * @param {Number} opts.caching.size How many entries should be kept in the cache, before the oldest is dropped.
+    */
+    constructor(private client: MatrixClient, private botClient: MatrixClient, opts: IntentOpts = {}) {
+        opts = opts || {};
+
+        opts.enablePresence = opts.enablePresence !== false;
+
+        if (opts.backingStore) {
+            if (!opts.backingStore.setPowerLevelContent ||
+                    !opts.backingStore.getPowerLevelContent ||
+                    !opts.backingStore.setMembership ||
+                    !opts.backingStore.getMembership) {
+                throw new Error("Intent backingStore missing required functions");
+            }
+        }
+        else {
+            this._membershipStates = {
+            //  room_id : "join|invite|leave|null"   null=unknown
+            };
+            this._powerLevels = {
+            //  room_id: event.content
+            };
+        }
+        this.opts = {
+            ...opts,
+            backingStore: opts.backingStore || {
+                getMembership: (roomId: string, userId: string) => {
+                    if (userId !== this.client.credentials.userId) {
+                        return null;
+                    }
+                    return this._membershipStates![roomId];
+                },
+                getPowerLevelContent: (roomId: string) => {
+                    return this._powerLevels![roomId];
+                },
+                setMembership: (roomId: string, userId: string, membership: MembershipState) => {
+                    if (userId !== this.client.credentials.userId) {
+                        return;
+                    }
+                    this._membershipStates![roomId] = membership;
+                },
+                setPowerLevelContent: (roomId: string, content: Record<string, any>) => {
+                    this._powerLevels![roomId] = content;
+                },
+            },
+            caching: {
+                size: opts.caching?.ttl || DEFAULT_CACHE_SIZE,
+                ttl: opts.caching?.ttl || DEFAULT_CACHE_TTL,
+            },
+            enablePresence: opts.enablePresence !== false,
+        };
+        this._requestCaches = {
+            profile: new ClientRequestCache(
+                this.opts.caching.ttl,
+                this.opts.caching.size,
+                (_: any, userId: string, info: string) => {
+                    return this.getProfileInfo(userId, info, false);
+                }
+            ),
+            roomstate: new ClientRequestCache(
+                this.opts.caching.ttl,
+                this.opts.caching.size,
+                (roomId: string) => {
+                    return this.roomState(roomId, false);
+                }
+            ),
+            event: new ClientRequestCache(
+                this.opts.caching.ttl,
+                this.opts.caching.size,
+                (_: any, roomId: string, eventId: string) => {
+                    return this.getEvent(roomId, eventId, false);
+                }
+            ),
+        };
+    }
+
+    /**
+     * Return the client this Intent is acting on behalf of.
+     * @return {MatrixClient} The client
+     */
+    getClient = () => {
+        return this.client;
+    };
+
+    /**
+     * <p>Send a plaintext message to a room.</p>
+     * This will automatically make the client join the room so they can send the
+     * message if they are not already joined. It will also make sure that the client
+     * has sufficient power level to do this.
+     * @param {string} roomId The room to send to.
+     * @param {string} text The text string to send.
+     * @return {Promise}
+     */
+    sendText = (roomId: string, text: string) => {
+        return this.sendMessage(roomId, {
+            body: text,
+            msgtype: "m.text"
+        });
+    };
+
+    /**
+     * <p>Set the name of a room.</p>
+     * This will automatically make the client join the room so they can set the
+     * name if they are not already joined. It will also make sure that the client
+     * has sufficient power level to do this.
+     * @param {string} roomId The room to send to.
+     * @param {string} name The room name.
+     * @return {Promise}
+     */
+    setRoomName = (roomId: string, name: string) => {
+        return this.sendStateEvent(roomId, "m.room.name", "", {
+            name: name
+        });
+    };
+
+    /**
+     * <p>Set the topic of a room.</p>
+     * This will automatically make the client join the room so they can set the
+     * topic if they are not already joined. It will also make sure that the client
+     * has sufficient power level to do this.
+     * @param {string} roomId The room to send to.
+     * @param {string} topic The room topic.
+     * @return {Promise}
+     */
+    setRoomTopic = (roomId: string, topic: string) => {
+        return this.sendStateEvent(roomId, "m.room.topic", "", {
+            topic: topic
+        });
+    };
+
+    /**
+     * <p>Set the avatar of a room.</p>
+     * This will automatically make the client join the room so they can set the
+     * topic if they are not already joined. It will also make sure that the client
+     * has sufficient power level to do this.
+     * @param roomId The room to send to.
+     * @param avatar The url of the avatar.
+     * @param info Extra information about the image. See m.room.avatar for details.
+     * @return {Promise}
+     */
+    setRoomAvatar = (roomId: string, avatar: string, info?: string) => {
+        const content = {
+            info,
+            url: avatar,
+        };
+        return this.sendStateEvent(roomId, "m.room.avatar", "", content);
+    };
+
+    /**
+     * <p>Send a typing event to a room.</p>
+     * This will automatically make the client join the room so they can send the
+     * typing event if they are not already joined.
+     * @param roomId The room to send to.
+     * @param {boolean} isTyping True if typing
+     * @return {Promise}
+     */
+    sendTyping = async(roomId: string, isTyping: boolean) => {
+        await this._ensureJoined(roomId);
+        await this._ensureHasPowerLevelFor(roomId, "m.typing");
+        return this.client.sendTyping(roomId, isTyping);
+    };
+
+    /**
+     * <p>Send a read receipt to a room.</p>
+     * This will automatically make the client join the room so they can send the
+     * receipt event if they are not already joined.
+     * @param{string} roomId The room to send to.
+     * @param{string} eventId The event ID to set the receipt mark to.
+     * @return {Promise}
+     */
+    sendReadReceipt = async(roomId: string, eventId: string) => {
+        const event = new MatrixEvent({
+            room_id: roomId,
+            event_id: eventId,
+        });
+        await this._ensureJoined(roomId);
+        return this.client.sendReadReceipt(event);
+    }
+
+    /**
+     * Set the power level of the given target.
+     * @param {string} roomId The room to set the power level in.
+     * @param {string} target The target user ID
+     * @param {number} level The desired level
+     * @return {Promise}
+     */
+    setPowerLevel = async(roomId: string, target: string, level: number) => {
+        await this._ensureJoined(roomId);
+        const event = await this._ensureHasPowerLevelFor(roomId, "m.room.power_levels");
+        return this.client.setPowerLevel(roomId, target, level, event);
+    };
+
+    /**
+     * <p>Send an <code>m.room.message</code> event to a room.</p>
+     * This will automatically make the client join the room so they can send the
+     * message if they are not already joined. It will also make sure that the client
+     * has sufficient power level to do this.
+     * @param {string} roomId The room to send to.
+     * @param {Object} content The event content
+     * @return {Promise}
+     */
+    sendMessage = (roomId: string, content: Record<string, any>) => {
+        return this.sendEvent(roomId, "m.room.message", content);
+    };
+
+    /**
+     * <p>Send a message event to a room.</p>
+     * This will automatically make the client join the room so they can send the
+     * message if they are not already joined. It will also make sure that the client
+     * has sufficient power level to do this.
+     * @param {string} roomId The room to send to.
+     * @param {string} type The event type
+     * @param {Object} content The event content
+     * @return {Promise}
+     */
+    sendEvent = async(roomId: string, type: string, content: Record<string, any>) => {
+        await this._ensureJoined(roomId);
+        await this._ensureHasPowerLevelFor(roomId, type);
+        return this._joinGuard(roomId, async() => (
+            this.client.sendEvent(roomId, type, content)
+        ));
+    };
+
+    /**
+     * <p>Send a state event to a room.</p>
+     * This will automatically make the client join the room so they can send the
+     * state if they are not already joined. It will also make sure that the client
+     * has sufficient power level to do this.
+     * @param {string} roomId The room to send to.
+     * @param {string} type The event type
+     * @param {string} skey The state key
+     * @param {Object} content The event content
+     * @return {Promise}
+     */
+    sendStateEvent = async(roomId: string, type: string, skey: string, content: Record<string,any>) => {
+        await this._ensureJoined(roomId);
+        await this._ensureHasPowerLevelFor(roomId, type);
+        return this._joinGuard(roomId, async() => (
+            this.client.sendStateEvent(roomId, type, content, skey)
+        ));
+    };
+
+    /**
+     * <p>Get the current room state for a room.</p>
+     * This will automatically make the client join the room so they can get the
+     * state if they are not already joined.
+     * @param {string} roomId The room to get the state from.
+     * @param {boolean} [useCache=false] Should the request attempt to lookup
+     * state from the cache.
+     * @return {Promise}
+     */
+    roomState = async (roomId: string, useCache=false) => {
+        await this._ensureJoined(roomId);
+        if (useCache) {
+            return this._requestCaches.roomstate.get(roomId);
+        }
+        return this.client.roomState(roomId);
+    };
+
+    /**
+     * Create a room with a set of options.
+     * @param {Object} opts Options.
+     * @param {boolean} opts.createAsClient True to create this room as a client and
+     * not the bot: the bot will not join. False to create this room as the bot and
+     * auto-join the client. Default: false.
+     * @param {Object} opts.options Options to pass to the client SDK /createRoom API.
+     * @return {Promise}
+     */
+    createRoom = async(opts: {createAsClient?: boolean, options: Record<string, any>}) => {
+        const cli = opts.createAsClient ? this.client : this.botClient;
+        const options = opts.options || {};
+        if (!opts.createAsClient) {
+            // invite the client if they aren't already
+            options.invite = options.invite || [];
+            if (options.invite.indexOf(this.client.credentials.userId) === -1) {
+                options.invite.push(this.client.credentials.userId);
+            }
+        }
+        // make sure that the thing doing the room creation isn't inviting itself
+        // else Synapse hard fails the operation with M_FORBIDDEN
+        if (options.invite && options.invite.indexOf(cli.credentials.userId) !== -1) {
+            options.invite.splice(options.invite.indexOf(cli.credentials.userId), 1);
+        }
+
+        await this._ensureRegistered();
+        const res = await cli.createRoom(options);
+        // create a fake power level event to give the room creator ops if we
+        // don't yet have a power level event.
+        if (this.opts.backingStore.getPowerLevelContent(res.room_id)) {
+            return res;
+        }
+        const users: Record<string,number> = {};
+        users[cli.credentials.userId] = 100;
+        this.opts.backingStore.setPowerLevelContent(res.room_id, {
+            users_default: 0,
+            events_default: 0,
+            state_default: 50,
+            users: users,
+            events: {}
+        });
+        return res;
+    };
+
+    /**
+     * <p>Invite a user to a room.</p>
+     * This will automatically make the client join the room so they can send the
+     * invite if they are not already joined.
+     * @param {string} roomId The room to invite the user to.
+     * @param {string} target The user ID to invite.
+     * @return {Promise} Resolved when invited, else rejected with an error.
+     */
+    invite = async(roomId: string, target: string) => {
+        await this._ensureJoined(roomId);
+        return this.client.invite(roomId, target);
+    };
+
+    /**
+     * <p>Kick a user from a room.</p>
+     * This will automatically make the client join the room so they can send the
+     * kick if they are not already joined.
+     * @param {string} roomId The room to kick the user from.
+     * @param {string} target The target of the kick operation.
+     * @param {string} reason Optional. The reason for the kick.
+     * @return {Promise} Resolved when kickked, else rejected with an error.
+     */
+    kick = async(roomId: string, target: string, reason: string) => {
+        await this._ensureJoined(roomId);
+        return this.client.kick(roomId, target, reason);
+    };
+
+    /**
+     * <p>Ban a user from a room.</p>
+     * This will automatically make the client join the room so they can send the
+     * ban if they are not already joined.
+     * @param {string} roomId The room to ban the user from.
+     * @param {string} target The target of the ban operation.
+     * @param {string} reason Optional. The reason for the ban.
+     * @return {Promise} Resolved when banned, else rejected with an error.
+     */
+    ban = async(roomId: string, target: string, reason: string) => {
+        await this._ensureJoined(roomId);
+        return this.client.ban(roomId, target, reason);
+    };
+
+    /**
+     * <p>Unban a user from a room.</p>
+     * This will automatically make the client join the room so they can send the
+     * unban if they are not already joined.
+     * @param {string} roomId The room to unban the user from.
+     * @param {string} target The target of the unban operation.
+     * @return {Promise} Resolved when unbanned, else rejected with an error.
+     */
+    unban = async(roomId: string, target: string) => {
+        await this._ensureJoined(roomId);
+        return this.client.unban(roomId, target);
+    };
+
+    /**
+     * <p>Join a room</p>
+     * This will automatically send an invite from the bot if it is an invite-only
+     * room, which may make the bot attempt to join the room if it isn't already.
+     * @param {string} roomId The room to join.
+     * @param {string[]} viaServers The server names to try and join through in
+     * addition to those that are automatically chosen.
+     * @return {Promise}
+     */
+    join = async(roomId: string, viaServers: string[]) => {
+        await this._ensureJoined(roomId, false, viaServers);
+    };
+
+    /**
+     * <p>Leave a room</p>
+     * This will no-op if the user isn't in the room.
+     * @param {string} roomId The room to leave.
+     * @return {Promise}
+     */
+    leave = async(roomId: string) => {
+        return this.client.leave(roomId);
+    };
+
+    /**
+     * <p>Get a user's profile information</p>
+     * @param {string} userId The ID of the user whose profile to return
+     * @param {string} info The profile field name to retrieve (e.g. 'displayname'
+     * or 'avatar_url'), or null to fetch the entire profile information.
+     * @param {boolean} [useCache=true] Should the request attempt to lookup
+     * state from the cache.
+     * @return {Promise} A Promise that resolves with the requested user's profile
+     * information
+     */
+    getProfileInfo = async(userId: string, info: string, useCache=true) => {
+        await this._ensureRegistered();
+        if (useCache) {
+            return this._requestCaches.profile.get(`${userId}:${info}`, userId, info);
+        }
+        return this.client.getProfileInfo(userId, info);
+    };
+
+    /**
+     * <p>Set the user's display name</p>
+     * @param {string} name The new display name
+     * @return {Promise}
+     */
+    setDisplayName = async(name: string) => {
+        await this._ensureRegistered();
+        return this.client.setDisplayName(name);
+    };
+
+    /**
+     * <p>Set the user's avatar URL</p>
+     * @param {string} url The new avatar URL
+     * @return {Promise}
+     */
+    setAvatarUrl = async(url: string) => {
+        await this._ensureRegistered();
+        return this.client.setAvatarUrl(url);
+    };
+
+    /**
+     * Create a new alias mapping.
+     * @param {string} alias The room alias to create
+     * @param {string} roomId The room ID the alias should point at.
+     * @return {Promise}
+     */
+    createAlias = async(alias: string, roomId: string) => {
+        await this._ensureRegistered();
+        return this.client.createAlias(alias, roomId);
+    };
+
+    /**
+     * Set the presence of this user.
+     * @param {string} presence One of "online", "offline" or "unavailable".
+     * @param {string} status_msg The status message to attach.
+     * @return {Promise} Resolves if the presence was set or no-oped, rejects otherwise.
+     */
+    setPresence = async(presence: string, status_msg?: string) => {
+        if (!this.opts.enablePresence) {
+            return;
+        }
+
+        await this._ensureRegistered();
+        return this.client.setPresence({presence, status_msg});
+    };
+
+    /**
+     * @typedef {
+     *       "m.event_not_handled"
+     *     | "m.event_too_old"
+     *     | "m.internal_error"
+     *     | "m.foreign_network_error"
+     *     | "m.event_unknown"
+     * } BridgeErrorReason
+     */
+
+    /**
+     * Signals that an error occured while handling an event by the bridge.
+     *
+     * **Warning**: This function is unstable and is likely to change pending the outcome
+     * of https://github.com/matrix-org/matrix-doc/pull/2162.
+     * @param {string} roomID ID of the room in which the error occured.
+     * @param {string} eventID ID of the event for which the error occured.
+     * @param {string} networkName Name of the bridged network.
+     * @param {BridgeErrorReason} reason The reason why the bridge error occured.
+     * @param {string} reason_body A human readable string d
+     * @param {string[]} affectedUsers Array of regex matching all affected users.
+     * @return {Promise}
+     */
+    unstableSignalBridgeError = async(
+        roomID: string,
+        eventID: string,
+        networkName: string,
+        reason: BridgeErrorReason,
+        affectedUsers: string[],
+    ) => {
+        return this.sendEvent(
+            roomID,
+            "de.nasnotfound.bridge_error",
+            {
+                network_name: networkName,
+                reason: reason,
+                affected_users: affectedUsers,
+                "m.relates_to": {
+                    rel_type: "m.reference",
+                    event_id: eventID,
+                },
+            }
+        );
+    }
+
+    /**
+     * Get an event in a room.
+     * This will automatically make the client join the room so they can get the
+     * event if they are not already joined.
+     * @param {string} roomId The room to fetch the event from.
+     * @param {string} eventId The eventId of the event to fetch.
+     * @param {boolean} [useCache=true] Should the request attempt to lookup from the cache.
+     * @return {Promise} Resolves with the content of the event, or rejects if not found.
+     */
+    getEvent = async(roomId: string, eventId: string, useCache=true) => {
+        await this._ensureRegistered();
+        if (useCache) {
+            return this._requestCaches.event.get(`${roomId}:${eventId}`, roomId, eventId);
+        }
+        return this.client.fetchRoomEvent(roomId, eventId);
+    };
+
+    /**
+     * Get a state event in a room.
+     * This will automatically make the client join the room so they can get the
+     * state if they are not already joined.
+     * @param {string} roomId The room to get the state from.
+     * @param {string} eventType The event type to fetch.
+     * @param {string} [stateKey=""] The state key of the event to fetch.
+     * @return {Promise}
+     */
+    getStateEvent = async(roomId: string, eventType: string, stateKey = "") => {
+        await this._ensureJoined(roomId);
+        return this.client.getStateEvent(roomId, eventType, stateKey);
+    };
+
+    /**
+     * Inform this Intent class of an incoming event. Various optimisations will be
+     * done if this is provided. For example, a /join request won't be sent out if
+     * it knows you've already been joined to the room. This function does nothing
+     * if a backing store was provided to the Intent.
+     * @param {Object} event The incoming event JSON
+     */
+    onEvent = (event: {type: string, content: Record<string, any>, state_key: any, room_id: string}) => {
+        if (!this._membershipStates || !this._powerLevels) {
+            return;
+        }
+        if (event.type === "m.room.member" &&
+                event.state_key === this.client.credentials.userId) {
+            this._membershipStates[event.room_id] = event.content.membership;
+        }
+        else if (event.type === "m.room.power_levels") {
+            this._powerLevels[event.room_id] = event.content;
+        }
+    };
+
+    // Guard a function which returns a promise which may reject if the user is not
+    // in the room. If the promise rejects, join the room and retry the function.
+    private _joinGuard = (roomId: string, promiseFn: () => Promise<any>) => {
+        return () => {
+            return promiseFn().catch(async(err) => {
+                if (err.errcode !== "M_FORBIDDEN") {
+                    // not a guardable error
+                    throw err;
+                }
+                await this._ensureJoined(roomId, true);
+                return promiseFn();
+            });
+        };
+    };
+
+    private _ensureJoined = async(
+        roomId: string, ignoreCache = false, viaServers?: string[], passthroughError = false
+    ) => {
+        const userId = this.client.credentials.userId;
+        const opts = {
+            syncRoom: false,
+            viaServers: viaServers ? viaServers : undefined,
+        };
+        if (this.opts.backingStore.getMembership(roomId, userId) === "join" && !ignoreCache) {
+            return Promise.resolve();
+        }
+
+        /* Logic:
+        if client /join:
+        SUCCESS
+        else if bot /invite client:
+        if client /join:
+            SUCCESS
+        else:
+            FAIL (client couldn't join)
+        else if bot /join:
+        if bot /invite client and client /join:
+            SUCCESS
+        else:
+            FAIL (bot couldn't invite)
+        else:
+        FAIL (bot can't get into the room)
+        */
+
+        const deferredPromise = Bluebird.defer();
+
+        const mark = (roomId: string, state: MembershipState) => {
+            this.opts.backingStore.setMembership(roomId, userId, state);
+            if (state === "join") {
+                deferredPromise.resolve();
+            }
+        }
+
+        const dontJoin = this.opts.dontJoin;
+
+        try {
+            await this._ensureRegistered();
+            if (dontJoin) {
+                deferredPromise.resolve();
+                return deferredPromise.promise;
+            }
+            try {
+                await this.client.joinRoom(roomId, opts);
+                mark(roomId, "join");
+            }
+            catch (ex) {
+                if (ex.errcode !== "M_FORBIDDEN") {
+                    throw ex;
+                }
+                try {
+                    // Try bot inviting client
+                    await this.botClient.invite(roomId, userId);
+                    await this.client.joinRoom(roomId, opts);
+                    mark(roomId, "join");
+                }
+                catch (_ex) {
+                    // Try bot joining
+                    await this.botClient.joinRoom(roomId, opts)
+                    await this.botClient.invite(roomId, userId);
+                    await this.client.joinRoom(roomId, opts);
+                    mark(roomId, "join");
+                }
+            }
+        }
+        catch (ex) {
+            deferredPromise.reject(passthroughError ? ex : Error("Failed to join room"));
+        }
+
+        return deferredPromise.promise;
+    };
+
+    private _ensureHasPowerLevelFor = (roomId: string, eventType: string) => {
+        if (this.opts.dontCheckPowerLevel && eventType !== "m.room.power_levels") {
+            return Promise.resolve();
+        }
+        const userId = this.client.credentials.userId;
+        const plContent = this.opts.backingStore.getPowerLevelContent(roomId);
+        let promise = Promise.resolve(plContent);
+        if (!plContent) {
+            promise = this.client.getStateEvent(roomId, "m.room.power_levels", "");
+        }
+        return promise.then((eventContent) => {
+            this.opts.backingStore.setPowerLevelContent(roomId, eventContent);
+            const event = {
+                content: eventContent,
+                room_id: roomId,
+                sender: "",
+                event_id: "_",
+                state_key: "",
+                type: "m.room.power_levels"
+            }
+            const powerLevelEvent = new MatrixEvent(event);
+            // What level do we need for this event type?
+            const defaultLevel = STATE_EVENT_TYPES.includes(eventType) ? event.content.state_default : event.content.events_default;
+            const requiredLevel = event.content.events[eventType] || defaultLevel;
+
+            // Parse out what level the client has by abusing the JS SDK
+            const roomMember = new RoomMember(roomId, userId);
+            roomMember.setPowerLevelEvent(powerLevelEvent);
+
+            if (requiredLevel > roomMember.powerLevel) {
+                // can the bot update our power level?
+                const bot = new RoomMember(roomId, this.botClient.credentials.userId);
+                bot.setPowerLevelEvent(powerLevelEvent);
+                const levelRequiredToModifyPowerLevels = event.content.events[
+                    "m.room.power_levels"
+                ] || event.content.state_default;
+                if (levelRequiredToModifyPowerLevels > bot.powerLevel) {
+                    // even the bot has no power here.. give up.
+                    throw new Error(
+                        "Cannot ensure client has power level for event " + eventType +
+                        " : client has " + roomMember.powerLevel + " and we require " +
+                        requiredLevel + " and the bot doesn't have permission to " +
+                        "edit the client's power level."
+                    );
+                }
+                // update the client's power level first
+                return this.botClient.setPowerLevel(
+                    roomId, userId, requiredLevel, powerLevelEvent
+                ).then(() => {
+                    // tweak the level for the client to reflect the new reality
+                    const userLevels = powerLevelEvent.getContent().users || {};
+                    userLevels[userId] = requiredLevel;
+                    powerLevelEvent.getContent().users = userLevels;
+                    return Promise.resolve(powerLevelEvent);
+                });
+            }
+            return Promise.resolve(powerLevelEvent);
+        });
+    };
+
+    private _ensureRegistered = async() => {
+        if (this.opts.registered) {
+            return "registered=true";
+        }
+        const userId = this.client.credentials.userId;
+        const localpart = new MatrixUser(userId).localpart;
+        try {
+            const res = await this.botClient.register(localpart);
+            this.opts.registered = true;
+            return res;
+        } catch(err) {
+            if (err.errcode === "M_USER_IN_USE") {
+                this.opts.registered = true;
+                return null;
+            }
+            throw err;
+        }
+    };
+}
+
+module.exports = Intent;

From 277693b5c216f87eccfd1180353803c8ec34b9ea Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Mon, 3 Aug 2020 22:56:32 +0200
Subject: [PATCH 02/30] Fix some linting issues

---
 src/components/intent.ts | 233 +++++++++++++++++----------------------
 1 file changed, 102 insertions(+), 131 deletions(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index e1827d0b..c06149ee 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -25,18 +25,18 @@ type MatrixClient = {
     },
     ban: (roomId: string, target: string, reason: string) => Promise<any>;
     createAlias: (alias: string, roomId: string) => Promise<any>;
-    createRoom: (opts: Record<string,any>) => Promise<any>;
+    createRoom: (opts: Record<string, unknown>) => Promise<any>;
     fetchRoomEvent: (roomId: string, eventId: string) => Promise<any>;
     getStateEvent: (roomId: string, eventType: string, stateKey: string) => Promise<any>;
     invite: (roomId: string, userId: string) => Promise<any>;
-    joinRoom: (roomId: string, opts: Record<string,any>) => Promise<any>;
+    joinRoom: (roomId: string, opts: Record<string, unknown>) => Promise<any>;
     kick: (roomId: string, target: string, reason: string) => Promise<any>;
     leave: (roomId: string) => Promise<any>;
     register: (localpart: string) => Promise<any>;
     roomState: (roomId: string) => Promise<any>;
-    sendEvent: (roomId: string, type: string, content: Record<string,any>) => Promise<any>;
+    sendEvent: (roomId: string, type: string, content: Record<string, unknown>) => Promise<any>;
     sendReadReceipt: (event: any) => Promise<any>;
-    sendStateEvent: (roomId: string, type: string, content: Record<string, any>, skey: string) => Promise<any>;
+    sendStateEvent: (roomId: string, type: string, content: Record<string, unknown>, skey: string) => Promise<any>;
     sendTyping: (roomId: string, isTyping: boolean) => Promise<any>;
     setAvatarUrl: (url: string) => Promise<any>;
     setDisplayName: (name: string) => Promise<any>;
@@ -53,9 +53,9 @@ type MembershipState = "join" | "invite" | "leave" | null; // null = unknown
 interface IntentOpts {
     backingStore?: {
         getMembership: (roomId: string, userId: string) => MembershipState,
-        getPowerLevelContent: (roomId: string) => Record<string, any>,
+        getPowerLevelContent: (roomId: string) => Record<string, unknown>,
         setMembership: (roomId: string, userId: string, membership: MembershipState) => void,
-        setPowerLevelContent: (roomId: string, content: Record<string, any>) => void,
+        setPowerLevelContent: (roomId: string, content: Record<string, unknown>) => void,
     },
     caching?: {
         ttl?: number,
@@ -65,7 +65,7 @@ interface IntentOpts {
     dontJoin?: boolean;
     enablePresence?: boolean;
     registered?: boolean;
-};
+}
 
 const STATE_EVENT_TYPES = [
     "m.room.name", "m.room.topic", "m.room.power_levels", "m.room.member",
@@ -83,9 +83,9 @@ class Intent {
     private opts: {
         backingStore: {
             getMembership: (roomId: string, userId: string) => MembershipState,
-            getPowerLevelContent: (roomId: string) => Record<string,any>,
+            getPowerLevelContent: (roomId: string) => Record<string, unknown>,
             setMembership: (roomId: string, userId: string, membership: MembershipState) => void,
-            setPowerLevelContent: (roomId: string, content: Record<string,any>) => void,
+            setPowerLevelContent: (roomId: string, content: Record<string, unknown>) => void,
         },
         caching: {
             ttl: number,
@@ -97,52 +97,52 @@ class Intent {
         registered?: boolean;
     };
     private _membershipStates?: Record<string,MembershipState>;
-    private _powerLevels?: Record<string,Record<string,any>>;
+    private _powerLevels?: Record<string,Record<string, unknown>>;
     
     /**
     * Create an entity which can fulfil the intent of a given user.
     * @constructor
-    * @param {MatrixClient} client The matrix client instance whose intent is being
+    * @param client The matrix client instance whose intent is being
     * fulfilled e.g. the entity joining the room when you call intent.join(roomId).
-    * @param {MatrixClient} botClient The client instance for the AS bot itself.
+    * @param botClient The client instance for the AS bot itself.
     * This will be used to perform more priveleged actions such as creating new
     * rooms, sending invites, etc.
-    * @param {Object} opts Options for this Intent instance.
-    * @param {boolean} opts.registered True to inform this instance that the client
+    * @param opts Options for this Intent instance.
+    * @param opts.registered True to inform this instance that the client
     * is already registered. No registration requests will be made from this Intent.
     * Default: false.
-    * @param {boolean} opts.dontCheckPowerLevel True to not check for the right power
+    * @param opts.dontCheckPowerLevel True to not check for the right power
     * level before sending events. Default: false.
     *
-    * @param {Object=} opts.backingStore An object with 4 functions, outlined below.
+    * @param opts.backingStore An object with 4 functions, outlined below.
     * If this Object is supplied, ALL 4 functions must be supplied. If this Object
     * is not supplied, the Intent will maintain its own backing store for membership
     * and power levels, which may scale badly for lots of users.
     *
-    * @param {Function} opts.backingStore.getMembership A function which is called with a
+    * @param opts.backingStore.getMembership A function which is called with a
     * room ID and user ID which should return the membership status of this user as
     * a string e.g "join". `null` should be returned if the membership is unknown.
     *
-    * @param {Function} opts.backingStore.getPowerLevelContent A function which is called
+    * @param opts.backingStore.getPowerLevelContent A function which is called
     * with a room ID which should return the power level content for this room, as an Object.
     * `null` should be returned if there is no known content.
     *
-    * @param {Function} opts.backingStore.setMembership A function with the signature:
+    * @param opts.backingStore.setMembership A function with the signature:
     * function(roomId, userId, membership) which will set the membership of the given user in
     * the given room. This has no return value.
     *
-    * @param {Function} opts.backingStore.setPowerLevelContent A function with the signature:
+    * @param opts.backingStore.setPowerLevelContent A function with the signature:
     * function(roomId, content) which will set the power level content in the given room.
     * This has no return value.
     *
-    * @param {boolean} opts.dontJoin True to not attempt to join a room before
+    * @param opts.dontJoin True to not attempt to join a room before
     * sending messages into it. The surrounding code will have to ensure the correct
     * membership state itself in this case. Default: false.
     *
-    * @param {boolean} [opts.enablePresence=true] True to send presence, false to no-op.
+    * @param opts.enablePresence True to send presence, false to no-op.
     *
-    * @param {Number} opts.caching.ttl How long requests can stay in the cache, in milliseconds.
-    * @param {Number} opts.caching.size How many entries should be kept in the cache, before the oldest is dropped.
+    * @param opts.caching.ttl How long requests can stay in the cache, in milliseconds.
+    * @param opts.caching.size How many entries should be kept in the cache, before the oldest is dropped.
     */
     constructor(private client: MatrixClient, private botClient: MatrixClient, opts: IntentOpts = {}) {
         opts = opts || {};
@@ -183,7 +183,7 @@ class Intent {
                     }
                     this._membershipStates![roomId] = membership;
                 },
-                setPowerLevelContent: (roomId: string, content: Record<string, any>) => {
+                setPowerLevelContent: (roomId: string, content: Record<string, unknown>) => {
                     this._powerLevels![roomId] = content;
                 },
             },
@@ -220,7 +220,7 @@ class Intent {
 
     /**
      * Return the client this Intent is acting on behalf of.
-     * @return {MatrixClient} The client
+     * @return The client
      */
     getClient = () => {
         return this.client;
@@ -231,9 +231,8 @@ class Intent {
      * This will automatically make the client join the room so they can send the
      * message if they are not already joined. It will also make sure that the client
      * has sufficient power level to do this.
-     * @param {string} roomId The room to send to.
-     * @param {string} text The text string to send.
-     * @return {Promise}
+     * @param roomId The room to send to.
+     * @param text The text string to send.
      */
     sendText = (roomId: string, text: string) => {
         return this.sendMessage(roomId, {
@@ -247,9 +246,8 @@ class Intent {
      * This will automatically make the client join the room so they can set the
      * name if they are not already joined. It will also make sure that the client
      * has sufficient power level to do this.
-     * @param {string} roomId The room to send to.
-     * @param {string} name The room name.
-     * @return {Promise}
+     * @param roomId The room to send to.
+     * @param name The room name.
      */
     setRoomName = (roomId: string, name: string) => {
         return this.sendStateEvent(roomId, "m.room.name", "", {
@@ -262,9 +260,8 @@ class Intent {
      * This will automatically make the client join the room so they can set the
      * topic if they are not already joined. It will also make sure that the client
      * has sufficient power level to do this.
-     * @param {string} roomId The room to send to.
-     * @param {string} topic The room topic.
-     * @return {Promise}
+     * @param roomId The room to send to.
+     * @param topic The room topic.
      */
     setRoomTopic = (roomId: string, topic: string) => {
         return this.sendStateEvent(roomId, "m.room.topic", "", {
@@ -280,7 +277,6 @@ class Intent {
      * @param roomId The room to send to.
      * @param avatar The url of the avatar.
      * @param info Extra information about the image. See m.room.avatar for details.
-     * @return {Promise}
      */
     setRoomAvatar = (roomId: string, avatar: string, info?: string) => {
         const content = {
@@ -295,8 +291,7 @@ class Intent {
      * This will automatically make the client join the room so they can send the
      * typing event if they are not already joined.
      * @param roomId The room to send to.
-     * @param {boolean} isTyping True if typing
-     * @return {Promise}
+     * @param isTyping True if typing
      */
     sendTyping = async(roomId: string, isTyping: boolean) => {
         await this._ensureJoined(roomId);
@@ -308,9 +303,8 @@ class Intent {
      * <p>Send a read receipt to a room.</p>
      * This will automatically make the client join the room so they can send the
      * receipt event if they are not already joined.
-     * @param{string} roomId The room to send to.
-     * @param{string} eventId The event ID to set the receipt mark to.
-     * @return {Promise}
+     * @param roomId The room to send to.
+     * @param eventId The event ID to set the receipt mark to.
      */
     sendReadReceipt = async(roomId: string, eventId: string) => {
         const event = new MatrixEvent({
@@ -323,10 +317,9 @@ class Intent {
 
     /**
      * Set the power level of the given target.
-     * @param {string} roomId The room to set the power level in.
-     * @param {string} target The target user ID
-     * @param {number} level The desired level
-     * @return {Promise}
+     * @param roomId The room to set the power level in.
+     * @param target The target user ID
+     * @param level The desired level
      */
     setPowerLevel = async(roomId: string, target: string, level: number) => {
         await this._ensureJoined(roomId);
@@ -339,11 +332,10 @@ class Intent {
      * This will automatically make the client join the room so they can send the
      * message if they are not already joined. It will also make sure that the client
      * has sufficient power level to do this.
-     * @param {string} roomId The room to send to.
-     * @param {Object} content The event content
-     * @return {Promise}
+     * @param roomId The room to send to.
+     * @param content The event content
      */
-    sendMessage = (roomId: string, content: Record<string, any>) => {
+    sendMessage = (roomId: string, content: Record<string, unknown>) => {
         return this.sendEvent(roomId, "m.room.message", content);
     };
 
@@ -352,12 +344,11 @@ class Intent {
      * This will automatically make the client join the room so they can send the
      * message if they are not already joined. It will also make sure that the client
      * has sufficient power level to do this.
-     * @param {string} roomId The room to send to.
-     * @param {string} type The event type
-     * @param {Object} content The event content
-     * @return {Promise}
+     * @param roomId The room to send to.
+     * @param type The event type
+     * @param content The event content
      */
-    sendEvent = async(roomId: string, type: string, content: Record<string, any>) => {
+    sendEvent = async(roomId: string, type: string, content: Record<string, unknown>) => {
         await this._ensureJoined(roomId);
         await this._ensureHasPowerLevelFor(roomId, type);
         return this._joinGuard(roomId, async() => (
@@ -370,13 +361,12 @@ class Intent {
      * This will automatically make the client join the room so they can send the
      * state if they are not already joined. It will also make sure that the client
      * has sufficient power level to do this.
-     * @param {string} roomId The room to send to.
-     * @param {string} type The event type
-     * @param {string} skey The state key
-     * @param {Object} content The event content
-     * @return {Promise}
+     * @param roomId The room to send to.
+     * @param type The event type
+     * @param skey The state key
+     * @param content The event content
      */
-    sendStateEvent = async(roomId: string, type: string, skey: string, content: Record<string,any>) => {
+    sendStateEvent = async(roomId: string, type: string, skey: string, content: Record<string, unknown>) => {
         await this._ensureJoined(roomId);
         await this._ensureHasPowerLevelFor(roomId, type);
         return this._joinGuard(roomId, async() => (
@@ -388,10 +378,9 @@ class Intent {
      * <p>Get the current room state for a room.</p>
      * This will automatically make the client join the room so they can get the
      * state if they are not already joined.
-     * @param {string} roomId The room to get the state from.
-     * @param {boolean} [useCache=false] Should the request attempt to lookup
+     * @param roomId The room to get the state from.
+     * @param useCache Should the request attempt to lookup
      * state from the cache.
-     * @return {Promise}
      */
     roomState = async (roomId: string, useCache=false) => {
         await this._ensureJoined(roomId);
@@ -403,12 +392,11 @@ class Intent {
 
     /**
      * Create a room with a set of options.
-     * @param {Object} opts Options.
-     * @param {boolean} opts.createAsClient True to create this room as a client and
+     * @param opts Options.
+     * @param opts.createAsClient True to create this room as a client and
      * not the bot: the bot will not join. False to create this room as the bot and
      * auto-join the client. Default: false.
-     * @param {Object} opts.options Options to pass to the client SDK /createRoom API.
-     * @return {Promise}
+     * @param opts.options Options to pass to the client SDK /createRoom API.
      */
     createRoom = async(opts: {createAsClient?: boolean, options: Record<string, any>}) => {
         const cli = opts.createAsClient ? this.client : this.botClient;
@@ -416,13 +404,13 @@ class Intent {
         if (!opts.createAsClient) {
             // invite the client if they aren't already
             options.invite = options.invite || [];
-            if (options.invite.indexOf(this.client.credentials.userId) === -1) {
+            if (!options.invite.includes(this.client.credentials.userId)) {
                 options.invite.push(this.client.credentials.userId);
             }
         }
         // make sure that the thing doing the room creation isn't inviting itself
         // else Synapse hard fails the operation with M_FORBIDDEN
-        if (options.invite && options.invite.indexOf(cli.credentials.userId) !== -1) {
+        if (options.invite && options.invite.includes(cli.credentials.userId)) {
             options.invite.splice(options.invite.indexOf(cli.credentials.userId), 1);
         }
 
@@ -449,9 +437,9 @@ class Intent {
      * <p>Invite a user to a room.</p>
      * This will automatically make the client join the room so they can send the
      * invite if they are not already joined.
-     * @param {string} roomId The room to invite the user to.
-     * @param {string} target The user ID to invite.
-     * @return {Promise} Resolved when invited, else rejected with an error.
+     * @param roomId The room to invite the user to.
+     * @param target The user ID to invite.
+     * @return Resolved when invited, else rejected with an error.
      */
     invite = async(roomId: string, target: string) => {
         await this._ensureJoined(roomId);
@@ -462,10 +450,10 @@ class Intent {
      * <p>Kick a user from a room.</p>
      * This will automatically make the client join the room so they can send the
      * kick if they are not already joined.
-     * @param {string} roomId The room to kick the user from.
-     * @param {string} target The target of the kick operation.
-     * @param {string} reason Optional. The reason for the kick.
-     * @return {Promise} Resolved when kickked, else rejected with an error.
+     * @param roomId The room to kick the user from.
+     * @param target The target of the kick operation.
+     * @param reason Optional. The reason for the kick.
+     * @return Resolved when kickked, else rejected with an error.
      */
     kick = async(roomId: string, target: string, reason: string) => {
         await this._ensureJoined(roomId);
@@ -476,10 +464,10 @@ class Intent {
      * <p>Ban a user from a room.</p>
      * This will automatically make the client join the room so they can send the
      * ban if they are not already joined.
-     * @param {string} roomId The room to ban the user from.
-     * @param {string} target The target of the ban operation.
-     * @param {string} reason Optional. The reason for the ban.
-     * @return {Promise} Resolved when banned, else rejected with an error.
+     * @param roomId The room to ban the user from.
+     * @param target The target of the ban operation.
+     * @param reason Optional. The reason for the ban.
+     * @return Resolved when banned, else rejected with an error.
      */
     ban = async(roomId: string, target: string, reason: string) => {
         await this._ensureJoined(roomId);
@@ -490,9 +478,9 @@ class Intent {
      * <p>Unban a user from a room.</p>
      * This will automatically make the client join the room so they can send the
      * unban if they are not already joined.
-     * @param {string} roomId The room to unban the user from.
-     * @param {string} target The target of the unban operation.
-     * @return {Promise} Resolved when unbanned, else rejected with an error.
+     * @param roomId The room to unban the user from.
+     * @param target The target of the unban operation.
+     * @return Resolved when unbanned, else rejected with an error.
      */
     unban = async(roomId: string, target: string) => {
         await this._ensureJoined(roomId);
@@ -503,10 +491,9 @@ class Intent {
      * <p>Join a room</p>
      * This will automatically send an invite from the bot if it is an invite-only
      * room, which may make the bot attempt to join the room if it isn't already.
-     * @param {string} roomId The room to join.
-     * @param {string[]} viaServers The server names to try and join through in
+     * @param roomId The room to join.
+     * @param viaServers The server names to try and join through in
      * addition to those that are automatically chosen.
-     * @return {Promise}
      */
     join = async(roomId: string, viaServers: string[]) => {
         await this._ensureJoined(roomId, false, viaServers);
@@ -515,8 +502,7 @@ class Intent {
     /**
      * <p>Leave a room</p>
      * This will no-op if the user isn't in the room.
-     * @param {string} roomId The room to leave.
-     * @return {Promise}
+     * @param roomId The room to leave.
      */
     leave = async(roomId: string) => {
         return this.client.leave(roomId);
@@ -524,12 +510,12 @@ class Intent {
 
     /**
      * <p>Get a user's profile information</p>
-     * @param {string} userId The ID of the user whose profile to return
-     * @param {string} info The profile field name to retrieve (e.g. 'displayname'
+     * @param userId The ID of the user whose profile to return
+     * @param info The profile field name to retrieve (e.g. 'displayname'
      * or 'avatar_url'), or null to fetch the entire profile information.
-     * @param {boolean} [useCache=true] Should the request attempt to lookup
+     * @param useCache Should the request attempt to lookup
      * state from the cache.
-     * @return {Promise} A Promise that resolves with the requested user's profile
+     * @return A Promise that resolves with the requested user's profile
      * information
      */
     getProfileInfo = async(userId: string, info: string, useCache=true) => {
@@ -542,8 +528,7 @@ class Intent {
 
     /**
      * <p>Set the user's display name</p>
-     * @param {string} name The new display name
-     * @return {Promise}
+     * @param name The new display name
      */
     setDisplayName = async(name: string) => {
         await this._ensureRegistered();
@@ -552,8 +537,7 @@ class Intent {
 
     /**
      * <p>Set the user's avatar URL</p>
-     * @param {string} url The new avatar URL
-     * @return {Promise}
+     * @param url The new avatar URL
      */
     setAvatarUrl = async(url: string) => {
         await this._ensureRegistered();
@@ -562,9 +546,8 @@ class Intent {
 
     /**
      * Create a new alias mapping.
-     * @param {string} alias The room alias to create
-     * @param {string} roomId The room ID the alias should point at.
-     * @return {Promise}
+     * @param alias The room alias to create
+     * @param roomId The room ID the alias should point at.
      */
     createAlias = async(alias: string, roomId: string) => {
         await this._ensureRegistered();
@@ -573,9 +556,9 @@ class Intent {
 
     /**
      * Set the presence of this user.
-     * @param {string} presence One of "online", "offline" or "unavailable".
-     * @param {string} status_msg The status message to attach.
-     * @return {Promise} Resolves if the presence was set or no-oped, rejects otherwise.
+     * @param presence One of "online", "offline" or "unavailable".
+     * @param status_msg The status message to attach.
+     * @return Resolves if the presence was set or no-oped, rejects otherwise.
      */
     setPresence = async(presence: string, status_msg?: string) => {
         if (!this.opts.enablePresence) {
@@ -586,28 +569,17 @@ class Intent {
         return this.client.setPresence({presence, status_msg});
     };
 
-    /**
-     * @typedef {
-     *       "m.event_not_handled"
-     *     | "m.event_too_old"
-     *     | "m.internal_error"
-     *     | "m.foreign_network_error"
-     *     | "m.event_unknown"
-     * } BridgeErrorReason
-     */
-
     /**
      * Signals that an error occured while handling an event by the bridge.
      *
      * **Warning**: This function is unstable and is likely to change pending the outcome
      * of https://github.com/matrix-org/matrix-doc/pull/2162.
-     * @param {string} roomID ID of the room in which the error occured.
-     * @param {string} eventID ID of the event for which the error occured.
-     * @param {string} networkName Name of the bridged network.
-     * @param {BridgeErrorReason} reason The reason why the bridge error occured.
-     * @param {string} reason_body A human readable string d
-     * @param {string[]} affectedUsers Array of regex matching all affected users.
-     * @return {Promise}
+     * @param roomID ID of the room in which the error occured.
+     * @param eventID ID of the event for which the error occured.
+     * @param networkName Name of the bridged network.
+     * @param reason The reason why the bridge error occured.
+     * @param reason_body A human readable string d
+     * @param affectedUsers Array of regex matching all affected users.
      */
     unstableSignalBridgeError = async(
         roomID: string,
@@ -635,10 +607,10 @@ class Intent {
      * Get an event in a room.
      * This will automatically make the client join the room so they can get the
      * event if they are not already joined.
-     * @param {string} roomId The room to fetch the event from.
-     * @param {string} eventId The eventId of the event to fetch.
-     * @param {boolean} [useCache=true] Should the request attempt to lookup from the cache.
-     * @return {Promise} Resolves with the content of the event, or rejects if not found.
+     * @param roomId The room to fetch the event from.
+     * @param eventId The eventId of the event to fetch.
+     * @param useCache Should the request attempt to lookup from the cache.
+     * @return Resolves with the content of the event, or rejects if not found.
      */
     getEvent = async(roomId: string, eventId: string, useCache=true) => {
         await this._ensureRegistered();
@@ -652,10 +624,9 @@ class Intent {
      * Get a state event in a room.
      * This will automatically make the client join the room so they can get the
      * state if they are not already joined.
-     * @param {string} roomId The room to get the state from.
-     * @param {string} eventType The event type to fetch.
-     * @param {string} [stateKey=""] The state key of the event to fetch.
-     * @return {Promise}
+     * @param roomId The room to get the state from.
+     * @param eventType The event type to fetch.
+     * @param [stateKey=""] The state key of the event to fetch.
      */
     getStateEvent = async(roomId: string, eventType: string, stateKey = "") => {
         await this._ensureJoined(roomId);
@@ -667,9 +638,9 @@ class Intent {
      * done if this is provided. For example, a /join request won't be sent out if
      * it knows you've already been joined to the room. This function does nothing
      * if a backing store was provided to the Intent.
-     * @param {Object} event The incoming event JSON
+     * @param event The incoming event JSON
      */
-    onEvent = (event: {type: string, content: Record<string, any>, state_key: any, room_id: string}) => {
+    onEvent = (event: {type: string, content: {membership: MembershipState}, state_key: any, room_id: string}) => {
         if (!this._membershipStates || !this._powerLevels) {
             return;
         }
@@ -786,7 +757,7 @@ class Intent {
         return promise.then((eventContent) => {
             this.opts.backingStore.setPowerLevelContent(roomId, eventContent);
             const event = {
-                content: eventContent,
+                content: eventContent as any,
                 room_id: roomId,
                 sender: "",
                 event_id: "_",
@@ -843,7 +814,7 @@ class Intent {
             const res = await this.botClient.register(localpart);
             this.opts.registered = true;
             return res;
-        } catch(err) {
+        } catch (err) {
             if (err.errcode === "M_USER_IN_USE") {
                 this.opts.registered = true;
                 return null;

From a953c5c5447ae85544a6d36fbeb28c03abe70de8 Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Mon, 3 Aug 2020 23:06:07 +0200
Subject: [PATCH 03/30] Don't always include visServers

---
 src/components/intent.ts | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index c06149ee..ef2a00b4 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -672,10 +672,12 @@ class Intent {
         roomId: string, ignoreCache = false, viaServers?: string[], passthroughError = false
     ) => {
         const userId = this.client.credentials.userId;
-        const opts = {
+        const opts: { syncRoom: boolean, viaServers?: string[]}  = {
             syncRoom: false,
-            viaServers: viaServers ? viaServers : undefined,
         };
+        if (viaServers) {
+            opts.viaServers = viaServers;
+        }
         if (this.opts.backingStore.getMembership(roomId, userId) === "join" && !ignoreCache) {
             return Promise.resolve();
         }

From fd1dff0598e92e68031137c13c51761e389b6be7 Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Tue, 4 Aug 2020 14:11:51 +0200
Subject: [PATCH 04/30] Refactor/fix ._joinGuard; use public explicitly and
 don't use arrow functions for the class methods

---
 src/components/intent.ts | 88 ++++++++++++++++++++--------------------
 1 file changed, 44 insertions(+), 44 deletions(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index ef2a00b4..eaedd668 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -222,7 +222,7 @@ class Intent {
      * Return the client this Intent is acting on behalf of.
      * @return The client
      */
-    getClient = () => {
+    public getClient() {
         return this.client;
     };
 
@@ -234,7 +234,7 @@ class Intent {
      * @param roomId The room to send to.
      * @param text The text string to send.
      */
-    sendText = (roomId: string, text: string) => {
+    public sendText(roomId: string, text: string) {
         return this.sendMessage(roomId, {
             body: text,
             msgtype: "m.text"
@@ -249,7 +249,7 @@ class Intent {
      * @param roomId The room to send to.
      * @param name The room name.
      */
-    setRoomName = (roomId: string, name: string) => {
+    public setRoomName(roomId: string, name: string) {
         return this.sendStateEvent(roomId, "m.room.name", "", {
             name: name
         });
@@ -263,7 +263,7 @@ class Intent {
      * @param roomId The room to send to.
      * @param topic The room topic.
      */
-    setRoomTopic = (roomId: string, topic: string) => {
+    public setRoomTopic(roomId: string, topic: string) {
         return this.sendStateEvent(roomId, "m.room.topic", "", {
             topic: topic
         });
@@ -278,7 +278,7 @@ class Intent {
      * @param avatar The url of the avatar.
      * @param info Extra information about the image. See m.room.avatar for details.
      */
-    setRoomAvatar = (roomId: string, avatar: string, info?: string) => {
+    public setRoomAvatar(roomId: string, avatar: string, info?: string) {
         const content = {
             info,
             url: avatar,
@@ -293,7 +293,7 @@ class Intent {
      * @param roomId The room to send to.
      * @param isTyping True if typing
      */
-    sendTyping = async(roomId: string, isTyping: boolean) => {
+    public async sendTyping(roomId: string, isTyping: boolean) {
         await this._ensureJoined(roomId);
         await this._ensureHasPowerLevelFor(roomId, "m.typing");
         return this.client.sendTyping(roomId, isTyping);
@@ -306,7 +306,7 @@ class Intent {
      * @param roomId The room to send to.
      * @param eventId The event ID to set the receipt mark to.
      */
-    sendReadReceipt = async(roomId: string, eventId: string) => {
+    public async sendReadReceipt(roomId: string, eventId: string) {
         const event = new MatrixEvent({
             room_id: roomId,
             event_id: eventId,
@@ -321,7 +321,7 @@ class Intent {
      * @param target The target user ID
      * @param level The desired level
      */
-    setPowerLevel = async(roomId: string, target: string, level: number) => {
+    public async setPowerLevel(roomId: string, target: string, level: number) {
         await this._ensureJoined(roomId);
         const event = await this._ensureHasPowerLevelFor(roomId, "m.room.power_levels");
         return this.client.setPowerLevel(roomId, target, level, event);
@@ -335,7 +335,7 @@ class Intent {
      * @param roomId The room to send to.
      * @param content The event content
      */
-    sendMessage = (roomId: string, content: Record<string, unknown>) => {
+    public sendMessage(roomId: string, content: Record<string, unknown>) {
         return this.sendEvent(roomId, "m.room.message", content);
     };
 
@@ -348,7 +348,7 @@ class Intent {
      * @param type The event type
      * @param content The event content
      */
-    sendEvent = async(roomId: string, type: string, content: Record<string, unknown>) => {
+    public async sendEvent(roomId: string, type: string, content: Record<string, unknown>) {
         await this._ensureJoined(roomId);
         await this._ensureHasPowerLevelFor(roomId, type);
         return this._joinGuard(roomId, async() => (
@@ -366,7 +366,7 @@ class Intent {
      * @param skey The state key
      * @param content The event content
      */
-    sendStateEvent = async(roomId: string, type: string, skey: string, content: Record<string, unknown>) => {
+    public async sendStateEvent(roomId: string, type: string, skey: string, content: Record<string, unknown>) {
         await this._ensureJoined(roomId);
         await this._ensureHasPowerLevelFor(roomId, type);
         return this._joinGuard(roomId, async() => (
@@ -382,7 +382,7 @@ class Intent {
      * @param useCache Should the request attempt to lookup
      * state from the cache.
      */
-    roomState = async (roomId: string, useCache=false) => {
+    public async roomState(roomId: string, useCache=false) {
         await this._ensureJoined(roomId);
         if (useCache) {
             return this._requestCaches.roomstate.get(roomId);
@@ -398,7 +398,7 @@ class Intent {
      * auto-join the client. Default: false.
      * @param opts.options Options to pass to the client SDK /createRoom API.
      */
-    createRoom = async(opts: {createAsClient?: boolean, options: Record<string, any>}) => {
+    public async createRoom(opts: {createAsClient?: boolean, options: Record<string, any>}) {
         const cli = opts.createAsClient ? this.client : this.botClient;
         const options = opts.options || {};
         if (!opts.createAsClient) {
@@ -441,7 +441,7 @@ class Intent {
      * @param target The user ID to invite.
      * @return Resolved when invited, else rejected with an error.
      */
-    invite = async(roomId: string, target: string) => {
+    public async invite(roomId: string, target: string) {
         await this._ensureJoined(roomId);
         return this.client.invite(roomId, target);
     };
@@ -455,7 +455,7 @@ class Intent {
      * @param reason Optional. The reason for the kick.
      * @return Resolved when kickked, else rejected with an error.
      */
-    kick = async(roomId: string, target: string, reason: string) => {
+    public async kick(roomId: string, target: string, reason: string) {
         await this._ensureJoined(roomId);
         return this.client.kick(roomId, target, reason);
     };
@@ -469,7 +469,7 @@ class Intent {
      * @param reason Optional. The reason for the ban.
      * @return Resolved when banned, else rejected with an error.
      */
-    ban = async(roomId: string, target: string, reason: string) => {
+    public async ban(roomId: string, target: string, reason: string) {
         await this._ensureJoined(roomId);
         return this.client.ban(roomId, target, reason);
     };
@@ -482,7 +482,7 @@ class Intent {
      * @param target The target of the unban operation.
      * @return Resolved when unbanned, else rejected with an error.
      */
-    unban = async(roomId: string, target: string) => {
+    public async unban(roomId: string, target: string) {
         await this._ensureJoined(roomId);
         return this.client.unban(roomId, target);
     };
@@ -495,7 +495,7 @@ class Intent {
      * @param viaServers The server names to try and join through in
      * addition to those that are automatically chosen.
      */
-    join = async(roomId: string, viaServers: string[]) => {
+    public async join(roomId: string, viaServers: string[]) {
         await this._ensureJoined(roomId, false, viaServers);
     };
 
@@ -504,7 +504,7 @@ class Intent {
      * This will no-op if the user isn't in the room.
      * @param roomId The room to leave.
      */
-    leave = async(roomId: string) => {
+    public async leave(roomId: string) {
         return this.client.leave(roomId);
     };
 
@@ -518,7 +518,7 @@ class Intent {
      * @return A Promise that resolves with the requested user's profile
      * information
      */
-    getProfileInfo = async(userId: string, info: string, useCache=true) => {
+    public async getProfileInfo(userId: string, info: string, useCache=true) {
         await this._ensureRegistered();
         if (useCache) {
             return this._requestCaches.profile.get(`${userId}:${info}`, userId, info);
@@ -530,7 +530,7 @@ class Intent {
      * <p>Set the user's display name</p>
      * @param name The new display name
      */
-    setDisplayName = async(name: string) => {
+    public async setDisplayName(name: string) {
         await this._ensureRegistered();
         return this.client.setDisplayName(name);
     };
@@ -539,7 +539,7 @@ class Intent {
      * <p>Set the user's avatar URL</p>
      * @param url The new avatar URL
      */
-    setAvatarUrl = async(url: string) => {
+    public async setAvatarUrl(url: string) {
         await this._ensureRegistered();
         return this.client.setAvatarUrl(url);
     };
@@ -549,7 +549,7 @@ class Intent {
      * @param alias The room alias to create
      * @param roomId The room ID the alias should point at.
      */
-    createAlias = async(alias: string, roomId: string) => {
+    public async createAlias(alias: string, roomId: string) {
         await this._ensureRegistered();
         return this.client.createAlias(alias, roomId);
     };
@@ -560,7 +560,7 @@ class Intent {
      * @param status_msg The status message to attach.
      * @return Resolves if the presence was set or no-oped, rejects otherwise.
      */
-    setPresence = async(presence: string, status_msg?: string) => {
+    public async setPresence(presence: string, status_msg?: string) {
         if (!this.opts.enablePresence) {
             return;
         }
@@ -581,13 +581,13 @@ class Intent {
      * @param reason_body A human readable string d
      * @param affectedUsers Array of regex matching all affected users.
      */
-    unstableSignalBridgeError = async(
+    public async unstableSignalBridgeError(
         roomID: string,
         eventID: string,
         networkName: string,
         reason: BridgeErrorReason,
         affectedUsers: string[],
-    ) => {
+    ) {
         return this.sendEvent(
             roomID,
             "de.nasnotfound.bridge_error",
@@ -612,7 +612,7 @@ class Intent {
      * @param useCache Should the request attempt to lookup from the cache.
      * @return Resolves with the content of the event, or rejects if not found.
      */
-    getEvent = async(roomId: string, eventId: string, useCache=true) => {
+    public async getEvent(roomId: string, eventId: string, useCache=true) {
         await this._ensureRegistered();
         if (useCache) {
             return this._requestCaches.event.get(`${roomId}:${eventId}`, roomId, eventId);
@@ -628,7 +628,7 @@ class Intent {
      * @param eventType The event type to fetch.
      * @param [stateKey=""] The state key of the event to fetch.
      */
-    getStateEvent = async(roomId: string, eventType: string, stateKey = "") => {
+    public async getStateEvent(roomId: string, eventType: string, stateKey = "") {
         await this._ensureJoined(roomId);
         return this.client.getStateEvent(roomId, eventType, stateKey);
     };
@@ -640,7 +640,7 @@ class Intent {
      * if a backing store was provided to the Intent.
      * @param event The incoming event JSON
      */
-    onEvent = (event: {type: string, content: {membership: MembershipState}, state_key: any, room_id: string}) => {
+    public onEvent(event: {type: string, content: {membership: MembershipState}, state_key: any, room_id: string}) {
         if (!this._membershipStates || !this._powerLevels) {
             return;
         }
@@ -655,22 +655,22 @@ class Intent {
 
     // Guard a function which returns a promise which may reject if the user is not
     // in the room. If the promise rejects, join the room and retry the function.
-    private _joinGuard = (roomId: string, promiseFn: () => Promise<any>) => {
-        return () => {
-            return promiseFn().catch(async(err) => {
-                if (err.errcode !== "M_FORBIDDEN") {
-                    // not a guardable error
-                    throw err;
-                }
-                await this._ensureJoined(roomId, true);
-                return promiseFn();
-            });
-        };
+    private async _joinGuard(roomId: string, promiseFn: () => Promise<any>) {
+        try {
+            return promiseFn();
+        } catch (err) {
+            if (err.errcode !== "M_FORBIDDEN") {
+                // not a guardable error
+                throw err;
+            }
+            await this._ensureJoined(roomId, true);
+            return promiseFn();
+        }
     };
 
-    private _ensureJoined = async(
+    private async _ensureJoined(
         roomId: string, ignoreCache = false, viaServers?: string[], passthroughError = false
-    ) => {
+    ) {
         const userId = this.client.credentials.userId;
         const opts: { syncRoom: boolean, viaServers?: string[]}  = {
             syncRoom: false,
@@ -746,7 +746,7 @@ class Intent {
         return deferredPromise.promise;
     };
 
-    private _ensureHasPowerLevelFor = (roomId: string, eventType: string) => {
+    private async _ensureHasPowerLevelFor(roomId: string, eventType: string) {
         if (this.opts.dontCheckPowerLevel && eventType !== "m.room.power_levels") {
             return Promise.resolve();
         }
@@ -806,7 +806,7 @@ class Intent {
         });
     };
 
-    private _ensureRegistered = async() => {
+    private async _ensureRegistered() {
         if (this.opts.registered) {
             return "registered=true";
         }

From 3db2a95b976f3c0e71255adc39e33425778390ee Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Tue, 4 Aug 2020 14:17:57 +0200
Subject: [PATCH 05/30] Fix some ESLint warnings

---
 src/components/intent.ts | 81 +++++++++++++++++++++-------------------
 1 file changed, 42 insertions(+), 39 deletions(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index eaedd668..4a28568c 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -60,7 +60,7 @@ interface IntentOpts {
     caching?: {
         ttl?: number,
         size?: number,
-    };
+    }
     dontCheckPowerLevel?: boolean;
     dontJoin?: boolean;
     enablePresence?: boolean;
@@ -79,7 +79,7 @@ class Intent {
         profile: ClientRequestCache,
         roomstate: ClientRequestCache,
         event: ClientRequestCache
-    };
+    }
     private opts: {
         backingStore: {
             getMembership: (roomId: string, userId: string) => MembershipState,
@@ -95,9 +95,9 @@ class Intent {
         dontJoin?: boolean;
         enablePresence: boolean;
         registered?: boolean;
-    };
-    private _membershipStates?: Record<string,MembershipState>;
-    private _powerLevels?: Record<string,Record<string, unknown>>;
+    }
+    private _membershipStates?: Record<string, MembershipState>;
+    private _powerLevels?: Record<string, Record<string, unknown>>;
     
     /**
     * Create an entity which can fulfil the intent of a given user.
@@ -224,7 +224,7 @@ class Intent {
      */
     public getClient() {
         return this.client;
-    };
+    }
 
     /**
      * <p>Send a plaintext message to a room.</p>
@@ -239,7 +239,7 @@ class Intent {
             body: text,
             msgtype: "m.text"
         });
-    };
+    }
 
     /**
      * <p>Set the name of a room.</p>
@@ -253,7 +253,7 @@ class Intent {
         return this.sendStateEvent(roomId, "m.room.name", "", {
             name: name
         });
-    };
+    }
 
     /**
      * <p>Set the topic of a room.</p>
@@ -267,7 +267,7 @@ class Intent {
         return this.sendStateEvent(roomId, "m.room.topic", "", {
             topic: topic
         });
-    };
+    }
 
     /**
      * <p>Set the avatar of a room.</p>
@@ -284,7 +284,7 @@ class Intent {
             url: avatar,
         };
         return this.sendStateEvent(roomId, "m.room.avatar", "", content);
-    };
+    }
 
     /**
      * <p>Send a typing event to a room.</p>
@@ -297,7 +297,7 @@ class Intent {
         await this._ensureJoined(roomId);
         await this._ensureHasPowerLevelFor(roomId, "m.typing");
         return this.client.sendTyping(roomId, isTyping);
-    };
+    }
 
     /**
      * <p>Send a read receipt to a room.</p>
@@ -325,7 +325,7 @@ class Intent {
         await this._ensureJoined(roomId);
         const event = await this._ensureHasPowerLevelFor(roomId, "m.room.power_levels");
         return this.client.setPowerLevel(roomId, target, level, event);
-    };
+    }
 
     /**
      * <p>Send an <code>m.room.message</code> event to a room.</p>
@@ -337,7 +337,7 @@ class Intent {
      */
     public sendMessage(roomId: string, content: Record<string, unknown>) {
         return this.sendEvent(roomId, "m.room.message", content);
-    };
+    }
 
     /**
      * <p>Send a message event to a room.</p>
@@ -354,7 +354,7 @@ class Intent {
         return this._joinGuard(roomId, async() => (
             this.client.sendEvent(roomId, type, content)
         ));
-    };
+    }
 
     /**
      * <p>Send a state event to a room.</p>
@@ -372,7 +372,7 @@ class Intent {
         return this._joinGuard(roomId, async() => (
             this.client.sendStateEvent(roomId, type, content, skey)
         ));
-    };
+    }
 
     /**
      * <p>Get the current room state for a room.</p>
@@ -388,7 +388,7 @@ class Intent {
             return this._requestCaches.roomstate.get(roomId);
         }
         return this.client.roomState(roomId);
-    };
+    }
 
     /**
      * Create a room with a set of options.
@@ -431,7 +431,7 @@ class Intent {
             events: {}
         });
         return res;
-    };
+    }
 
     /**
      * <p>Invite a user to a room.</p>
@@ -444,7 +444,7 @@ class Intent {
     public async invite(roomId: string, target: string) {
         await this._ensureJoined(roomId);
         return this.client.invite(roomId, target);
-    };
+    }
 
     /**
      * <p>Kick a user from a room.</p>
@@ -458,7 +458,7 @@ class Intent {
     public async kick(roomId: string, target: string, reason: string) {
         await this._ensureJoined(roomId);
         return this.client.kick(roomId, target, reason);
-    };
+    }
 
     /**
      * <p>Ban a user from a room.</p>
@@ -472,7 +472,7 @@ class Intent {
     public async ban(roomId: string, target: string, reason: string) {
         await this._ensureJoined(roomId);
         return this.client.ban(roomId, target, reason);
-    };
+    }
 
     /**
      * <p>Unban a user from a room.</p>
@@ -485,7 +485,7 @@ class Intent {
     public async unban(roomId: string, target: string) {
         await this._ensureJoined(roomId);
         return this.client.unban(roomId, target);
-    };
+    }
 
     /**
      * <p>Join a room</p>
@@ -497,7 +497,7 @@ class Intent {
      */
     public async join(roomId: string, viaServers: string[]) {
         await this._ensureJoined(roomId, false, viaServers);
-    };
+    }
 
     /**
      * <p>Leave a room</p>
@@ -506,7 +506,7 @@ class Intent {
      */
     public async leave(roomId: string) {
         return this.client.leave(roomId);
-    };
+    }
 
     /**
      * <p>Get a user's profile information</p>
@@ -524,7 +524,7 @@ class Intent {
             return this._requestCaches.profile.get(`${userId}:${info}`, userId, info);
         }
         return this.client.getProfileInfo(userId, info);
-    };
+    }
 
     /**
      * <p>Set the user's display name</p>
@@ -533,7 +533,7 @@ class Intent {
     public async setDisplayName(name: string) {
         await this._ensureRegistered();
         return this.client.setDisplayName(name);
-    };
+    }
 
     /**
      * <p>Set the user's avatar URL</p>
@@ -542,7 +542,7 @@ class Intent {
     public async setAvatarUrl(url: string) {
         await this._ensureRegistered();
         return this.client.setAvatarUrl(url);
-    };
+    }
 
     /**
      * Create a new alias mapping.
@@ -552,7 +552,7 @@ class Intent {
     public async createAlias(alias: string, roomId: string) {
         await this._ensureRegistered();
         return this.client.createAlias(alias, roomId);
-    };
+    }
 
     /**
      * Set the presence of this user.
@@ -567,7 +567,7 @@ class Intent {
 
         await this._ensureRegistered();
         return this.client.setPresence({presence, status_msg});
-    };
+    }
 
     /**
      * Signals that an error occured while handling an event by the bridge.
@@ -618,7 +618,7 @@ class Intent {
             return this._requestCaches.event.get(`${roomId}:${eventId}`, roomId, eventId);
         }
         return this.client.fetchRoomEvent(roomId, eventId);
-    };
+    }
 
     /**
      * Get a state event in a room.
@@ -631,7 +631,7 @@ class Intent {
     public async getStateEvent(roomId: string, eventType: string, stateKey = "") {
         await this._ensureJoined(roomId);
         return this.client.getStateEvent(roomId, eventType, stateKey);
-    };
+    }
 
     /**
      * Inform this Intent class of an incoming event. Various optimisations will be
@@ -640,6 +640,7 @@ class Intent {
      * if a backing store was provided to the Intent.
      * @param event The incoming event JSON
      */
+    // eslint-disable-next-line camelcase
     public onEvent(event: {type: string, content: {membership: MembershipState}, state_key: any, room_id: string}) {
         if (!this._membershipStates || !this._powerLevels) {
             return;
@@ -651,7 +652,7 @@ class Intent {
         else if (event.type === "m.room.power_levels") {
             this._powerLevels[event.room_id] = event.content;
         }
-    };
+    }
 
     // Guard a function which returns a promise which may reject if the user is not
     // in the room. If the promise rejects, join the room and retry the function.
@@ -666,13 +667,13 @@ class Intent {
             await this._ensureJoined(roomId, true);
             return promiseFn();
         }
-    };
+    }
 
     private async _ensureJoined(
         roomId: string, ignoreCache = false, viaServers?: string[], passthroughError = false
     ) {
         const userId = this.client.credentials.userId;
-        const opts: { syncRoom: boolean, viaServers?: string[]}  = {
+        const opts: { syncRoom: boolean, viaServers?: string[] } = {
             syncRoom: false,
         };
         if (viaServers) {
@@ -701,8 +702,8 @@ class Intent {
 
         const deferredPromise = Bluebird.defer();
 
-        const mark = (roomId: string, state: MembershipState) => {
-            this.opts.backingStore.setMembership(roomId, userId, state);
+        const mark = (room: string, state: MembershipState) => {
+            this.opts.backingStore.setMembership(room, userId, state);
             if (state === "join") {
                 deferredPromise.resolve();
             }
@@ -744,7 +745,7 @@ class Intent {
         }
 
         return deferredPromise.promise;
-    };
+    }
 
     private async _ensureHasPowerLevelFor(roomId: string, eventType: string) {
         if (this.opts.dontCheckPowerLevel && eventType !== "m.room.power_levels") {
@@ -768,7 +769,9 @@ class Intent {
             }
             const powerLevelEvent = new MatrixEvent(event);
             // What level do we need for this event type?
-            const defaultLevel = STATE_EVENT_TYPES.includes(eventType) ? event.content.state_default : event.content.events_default;
+            const defaultLevel = STATE_EVENT_TYPES.includes(eventType)
+                ? event.content.state_default
+                : event.content.events_default;
             const requiredLevel = event.content.events[eventType] || defaultLevel;
 
             // Parse out what level the client has by abusing the JS SDK
@@ -804,7 +807,7 @@ class Intent {
             }
             return Promise.resolve(powerLevelEvent);
         });
-    };
+    }
 
     private async _ensureRegistered() {
         if (this.opts.registered) {
@@ -823,7 +826,7 @@ class Intent {
             }
             throw err;
         }
-    };
+    }
 }
 
 module.exports = Intent;

From 9fe8d0542ca4251f20e5c51050b5fc68849ab48f Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Tue, 4 Aug 2020 14:33:24 +0200
Subject: [PATCH 06/30] Fix more ESLint warnings

---
 src/components/intent.ts | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index 4a28568c..c0b9ab9c 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -41,12 +41,14 @@ type MatrixClient = {
     setAvatarUrl: (url: string) => Promise<any>;
     setDisplayName: (name: string) => Promise<any>;
     setPowerLevel: (roomId: string, target: string, level: number, event: any) => Promise<any>;
+    // eslint-disable-next-line camelcase
     setPresence: (presence: { presence: string, status_msg?: string }) => Promise<any>;
     getProfileInfo: (userId: string, info: string) => Promise<any>;
     unban: (roomId: string, target: string) => Promise<any>;
 };
 
-type BridgeErrorReason = "m.event_not_handled" | "m.event_too_old" | "m.internal_error" | "m.foreign_network_error" | "m.event_unknown";
+type BridgeErrorReason = "m.event_not_handled" | "m.event_too_old"
+    | "m.internal_error" | "m.foreign_network_error" | "m.event_unknown";
 
 type MembershipState = "join" | "invite" | "leave" | null; // null = unknown
 
@@ -560,6 +562,7 @@ class Intent {
      * @param status_msg The status message to attach.
      * @return Resolves if the presence was set or no-oped, rejects otherwise.
      */
+    // eslint-disable-next-line camelcase
     public async setPresence(presence: string, status_msg?: string) {
         if (!this.opts.enablePresence) {
             return;

From 142d306d85c0fccbd32c5b9a9b11bb11c3e9b854 Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Tue, 4 Aug 2020 20:28:23 +0200
Subject: [PATCH 07/30] Don't use any

---
 src/components/intent.ts | 105 ++++++++++++++++++++-------------------
 1 file changed, 55 insertions(+), 50 deletions(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index c0b9ab9c..2c579139 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -16,6 +16,7 @@ limitations under the License.
 import Bluebird from "bluebird";
 import MatrixUser from "../models/users/matrix";
 import JsSdk from "matrix-js-sdk";
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 const { MatrixEvent, RoomMember } = JsSdk as any;
 import ClientRequestCache from "./client-request-cache";
 
@@ -23,28 +24,28 @@ type MatrixClient = {
     credentials: {
         userId: string;
     },
-    ban: (roomId: string, target: string, reason: string) => Promise<any>;
-    createAlias: (alias: string, roomId: string) => Promise<any>;
-    createRoom: (opts: Record<string, unknown>) => Promise<any>;
-    fetchRoomEvent: (roomId: string, eventId: string) => Promise<any>;
-    getStateEvent: (roomId: string, eventType: string, stateKey: string) => Promise<any>;
-    invite: (roomId: string, userId: string) => Promise<any>;
-    joinRoom: (roomId: string, opts: Record<string, unknown>) => Promise<any>;
-    kick: (roomId: string, target: string, reason: string) => Promise<any>;
-    leave: (roomId: string) => Promise<any>;
-    register: (localpart: string) => Promise<any>;
-    roomState: (roomId: string) => Promise<any>;
-    sendEvent: (roomId: string, type: string, content: Record<string, unknown>) => Promise<any>;
-    sendReadReceipt: (event: any) => Promise<any>;
-    sendStateEvent: (roomId: string, type: string, content: Record<string, unknown>, skey: string) => Promise<any>;
-    sendTyping: (roomId: string, isTyping: boolean) => Promise<any>;
-    setAvatarUrl: (url: string) => Promise<any>;
-    setDisplayName: (name: string) => Promise<any>;
-    setPowerLevel: (roomId: string, target: string, level: number, event: any) => Promise<any>;
+    ban: (roomId: string, target: string, reason: string) => Promise<unknown>;
+    createAlias: (alias: string, roomId: string) => Promise<unknown>;
+    createRoom: (opts: Record<string, unknown>) => Promise<unknown>;
+    fetchRoomEvent: (roomId: string, eventId: string) => Promise<unknown>;
+    getStateEvent: (roomId: string, eventType: string, stateKey: string) => Promise<unknown>;
+    invite: (roomId: string, userId: string) => Promise<unknown>;
+    joinRoom: (roomId: string, opts: Record<string, unknown>) => Promise<unknown>;
+    kick: (roomId: string, target: string, reason: string) => Promise<unknown>;
+    leave: (roomId: string) => Promise<unknown>;
+    register: (localpart: string) => Promise<unknown>;
+    roomState: (roomId: string) => Promise<unknown>;
+    sendEvent: (roomId: string, type: string, content: Record<string, unknown>) => Promise<unknown>;
+    sendReadReceipt: (event: unknown) => Promise<unknown>;
+    sendStateEvent: (roomId: string, type: string, content: Record<string, unknown>, skey: string) => Promise<unknown>;
+    sendTyping: (roomId: string, isTyping: boolean) => Promise<unknown>;
+    setAvatarUrl: (url: string) => Promise<unknown>;
+    setDisplayName: (name: string) => Promise<unknown>;
+    setPowerLevel: (roomId: string, target: string, level: number, event: unknown) => Promise<unknown>;
     // eslint-disable-next-line camelcase
-    setPresence: (presence: { presence: string, status_msg?: string }) => Promise<any>;
-    getProfileInfo: (userId: string, info: string) => Promise<any>;
-    unban: (roomId: string, target: string) => Promise<any>;
+    setPresence: (presence: { presence: string, status_msg?: string }) => Promise<unknown>;
+    getProfileInfo: (userId: string, info: string) => Promise<unknown>;
+    unban: (roomId: string, target: string) => Promise<unknown>;
 };
 
 type BridgeErrorReason = "m.event_not_handled" | "m.event_too_old"
@@ -98,9 +99,10 @@ class Intent {
         enablePresence: boolean;
         registered?: boolean;
     }
-    private _membershipStates?: Record<string, MembershipState>;
-    private _powerLevels?: Record<string, Record<string, unknown>>;
-    
+    // These two are only used if no opts.backingStore is provided to the constructor.
+    private _membershipStates: Record<string, MembershipState> = {};
+    private _powerLevels: Record<string, Record<string, unknown>> = {};
+
     /**
     * Create an entity which can fulfil the intent of a given user.
     * @constructor
@@ -159,14 +161,6 @@ class Intent {
                 throw new Error("Intent backingStore missing required functions");
             }
         }
-        else {
-            this._membershipStates = {
-            //  room_id : "join|invite|leave|null"   null=unknown
-            };
-            this._powerLevels = {
-            //  room_id: event.content
-            };
-        }
         this.opts = {
             ...opts,
             backingStore: opts.backingStore || {
@@ -174,19 +168,19 @@ class Intent {
                     if (userId !== this.client.credentials.userId) {
                         return null;
                     }
-                    return this._membershipStates![roomId];
+                    return this._membershipStates[roomId];
                 },
                 getPowerLevelContent: (roomId: string) => {
-                    return this._powerLevels![roomId];
+                    return this._powerLevels[roomId];
                 },
                 setMembership: (roomId: string, userId: string, membership: MembershipState) => {
                     if (userId !== this.client.credentials.userId) {
                         return;
                     }
-                    this._membershipStates![roomId] = membership;
+                    this._membershipStates[roomId] = membership;
                 },
                 setPowerLevelContent: (roomId: string, content: Record<string, unknown>) => {
-                    this._powerLevels![roomId] = content;
+                    this._powerLevels[roomId] = content;
                 },
             },
             caching: {
@@ -199,7 +193,7 @@ class Intent {
             profile: new ClientRequestCache(
                 this.opts.caching.ttl,
                 this.opts.caching.size,
-                (_: any, userId: string, info: string) => {
+                (_: unknown, userId: string, info: string) => {
                     return this.getProfileInfo(userId, info, false);
                 }
             ),
@@ -213,7 +207,7 @@ class Intent {
             event: new ClientRequestCache(
                 this.opts.caching.ttl,
                 this.opts.caching.size,
-                (_: any, roomId: string, eventId: string) => {
+                (_: unknown, roomId: string, eventId: string) => {
                     return this.getEvent(roomId, eventId, false);
                 }
             ),
@@ -400,32 +394,41 @@ class Intent {
      * auto-join the client. Default: false.
      * @param opts.options Options to pass to the client SDK /createRoom API.
      */
-    public async createRoom(opts: {createAsClient?: boolean, options: Record<string, any>}) {
+    public async createRoom(opts: { createAsClient?: boolean, options: Record<string, unknown>}) {
         const cli = opts.createAsClient ? this.client : this.botClient;
         const options = opts.options || {};
         if (!opts.createAsClient) {
             // invite the client if they aren't already
             options.invite = options.invite || [];
-            if (!options.invite.includes(this.client.credentials.userId)) {
+            if (Array.isArray(options.invite) && !options.invite.includes(this.client.credentials.userId)) {
                 options.invite.push(this.client.credentials.userId);
             }
         }
         // make sure that the thing doing the room creation isn't inviting itself
         // else Synapse hard fails the operation with M_FORBIDDEN
-        if (options.invite && options.invite.includes(cli.credentials.userId)) {
+        if (Array.isArray(options.invite) && options.invite.includes(cli.credentials.userId)) {
             options.invite.splice(options.invite.indexOf(cli.credentials.userId), 1);
         }
 
         await this._ensureRegistered();
         const res = await cli.createRoom(options);
+        if (typeof res !== "object" || !res) {
+            const type = res === null ? "null" : typeof res;
+            throw Error(`Expected Matrix Server to answer createRoom with an object, got ${type}.`);
+        }
+        const roomId = (res as Record<string, unknown>).room_id;
+        if (typeof roomId !== "string") {
+            const type = typeof roomId;
+            throw Error(`Expected Matrix Server to answer createRoom with a room_id that is a string, got ${type}.`);
+        }
         // create a fake power level event to give the room creator ops if we
         // don't yet have a power level event.
-        if (this.opts.backingStore.getPowerLevelContent(res.room_id)) {
+        if (this.opts.backingStore.getPowerLevelContent(roomId)) {
             return res;
         }
-        const users: Record<string,number> = {};
+        const users: Record<string, number> = {};
         users[cli.credentials.userId] = 100;
-        this.opts.backingStore.setPowerLevelContent(res.room_id, {
+        this.opts.backingStore.setPowerLevelContent(roomId, {
             users_default: 0,
             events_default: 0,
             state_default: 50,
@@ -565,7 +568,7 @@ class Intent {
     // eslint-disable-next-line camelcase
     public async setPresence(presence: string, status_msg?: string) {
         if (!this.opts.enablePresence) {
-            return;
+            return undefined;
         }
 
         await this._ensureRegistered();
@@ -644,7 +647,7 @@ class Intent {
      * @param event The incoming event JSON
      */
     // eslint-disable-next-line camelcase
-    public onEvent(event: {type: string, content: {membership: MembershipState}, state_key: any, room_id: string}) {
+    public onEvent(event: {type: string, content: {membership: MembershipState}, state_key: unknown, room_id: string}) {
         if (!this._membershipStates || !this._powerLevels) {
             return;
         }
@@ -659,10 +662,11 @@ class Intent {
 
     // Guard a function which returns a promise which may reject if the user is not
     // in the room. If the promise rejects, join the room and retry the function.
-    private async _joinGuard(roomId: string, promiseFn: () => Promise<any>) {
+    private async _joinGuard(roomId: string, promiseFn: () => Promise<unknown>) {
         try {
             return promiseFn();
-        } catch (err) {
+        }
+        catch (err) {
             if (err.errcode !== "M_FORBIDDEN") {
                 // not a guardable error
                 throw err;
@@ -763,7 +767,7 @@ class Intent {
         return promise.then((eventContent) => {
             this.opts.backingStore.setPowerLevelContent(roomId, eventContent);
             const event = {
-                content: eventContent as any,
+                content: eventContent,
                 room_id: roomId,
                 sender: "",
                 event_id: "_",
@@ -822,7 +826,8 @@ class Intent {
             const res = await this.botClient.register(localpart);
             this.opts.registered = true;
             return res;
-        } catch (err) {
+        }
+        catch (err) {
             if (err.errcode === "M_USER_IN_USE") {
                 this.opts.registered = true;
                 return null;

From ff12e3156f3ad956088c37eedee23b5e79e6ec43 Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Mon, 3 Aug 2020 23:04:40 +0200
Subject: [PATCH 08/30] Silence ESLint errors in event-queue

---
 src/components/event-queue.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/event-queue.ts b/src/components/event-queue.ts
index cc1ef549..5720624b 100644
--- a/src/components/event-queue.ts
+++ b/src/components/event-queue.ts
@@ -14,7 +14,7 @@ limitations under the License.
 */
 import Bluebird from "bluebird";
 
-type DataReady = Promise<object>;
+type DataReady = Promise<Record<string, unknown>>;
 
 // It's an event, which has no type yet.
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -52,6 +52,7 @@ export class EventQueue {
      * @param {IMatrixEvent} event The event to enqueue.
      * @param {Promise<object>} dataReady Promise containing data related to the event.
      */
+    // eslint-disable-next-line camelcase
     public push(event: {room_id: string}, dataReady: DataReady) {
         const queue = this.getQueue(event);
         queue.events.push({
@@ -59,6 +60,7 @@ export class EventQueue {
         });
     }
 
+    // eslint-disable-next-line camelcase
     private getQueue(event: {room_id: string}) {
         const identifier = this.type === "per_room" ? event.room_id : "none";
         if (!this.queues[identifier]) {

From 4637c0b282ccdec4dae32f2e7c6d65ef4e02deb4 Mon Sep 17 00:00:00 2001
From: Will Hunt <halfshot@matrix.org>
Date: Thu, 6 Aug 2020 14:05:41 +0100
Subject: [PATCH 09/30] linting

---
 src/components/intent.ts | 31 +++++++++++++++++++++++--------
 1 file changed, 23 insertions(+), 8 deletions(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index 2c579139..8a7eea69 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -56,9 +56,9 @@ type MembershipState = "join" | "invite" | "leave" | null; // null = unknown
 interface IntentOpts {
     backingStore?: {
         getMembership: (roomId: string, userId: string) => MembershipState,
-        getPowerLevelContent: (roomId: string) => Record<string, unknown>,
+        getPowerLevelContent: (roomId: string) => PowerLevelContent,
         setMembership: (roomId: string, userId: string, membership: MembershipState) => void,
-        setPowerLevelContent: (roomId: string, content: Record<string, unknown>) => void,
+        setPowerLevelContent: (roomId: string, content: PowerLevelContent) => void,
     },
     caching?: {
         ttl?: number,
@@ -77,6 +77,21 @@ const STATE_EVENT_TYPES = [
 const DEFAULT_CACHE_TTL = 90000;
 const DEFAULT_CACHE_SIZE = 1024;
 
+type PowerLevelContent = {
+    // eslint-disable-next-line camelcase
+    state_default: number;
+    // eslint-disable-next-line camelcase
+    events_default: number;
+    // eslint-disable-next-line camelcase
+    users_default: number;
+    users: {
+        [userId: string]: number;
+    },
+    events: {
+        [eventType: string]: number;
+    }
+};
+
 class Intent {
     private _requestCaches: {
         profile: ClientRequestCache,
@@ -86,9 +101,9 @@ class Intent {
     private opts: {
         backingStore: {
             getMembership: (roomId: string, userId: string) => MembershipState,
-            getPowerLevelContent: (roomId: string) => Record<string, unknown>,
+            getPowerLevelContent: (roomId: string) => PowerLevelContent,
             setMembership: (roomId: string, userId: string, membership: MembershipState) => void,
-            setPowerLevelContent: (roomId: string, content: Record<string, unknown>) => void,
+            setPowerLevelContent: (roomId: string, content: PowerLevelContent) => void,
         },
         caching: {
             ttl: number,
@@ -101,7 +116,7 @@ class Intent {
     }
     // These two are only used if no opts.backingStore is provided to the constructor.
     private _membershipStates: Record<string, MembershipState> = {};
-    private _powerLevels: Record<string, Record<string, unknown>> = {};
+    private _powerLevels: Record<string, PowerLevelContent> = {};
 
     /**
     * Create an entity which can fulfil the intent of a given user.
@@ -179,7 +194,7 @@ class Intent {
                     }
                     this._membershipStates[roomId] = membership;
                 },
-                setPowerLevelContent: (roomId: string, content: Record<string, unknown>) => {
+                setPowerLevelContent: (roomId: string, content: PowerLevelContent) => {
                     this._powerLevels[roomId] = content;
                 },
             },
@@ -656,7 +671,7 @@ class Intent {
             this._membershipStates[event.room_id] = event.content.membership;
         }
         else if (event.type === "m.room.power_levels") {
-            this._powerLevels[event.room_id] = event.content;
+            this._powerLevels[event.room_id] = event.content as unknown as PowerLevelContent;
         }
     }
 
@@ -762,7 +777,7 @@ class Intent {
         const plContent = this.opts.backingStore.getPowerLevelContent(roomId);
         let promise = Promise.resolve(plContent);
         if (!plContent) {
-            promise = this.client.getStateEvent(roomId, "m.room.power_levels", "");
+            promise = this.client.getStateEvent(roomId, "m.room.power_levels", "") as Promise<PowerLevelContent>;
         }
         return promise.then((eventContent) => {
             this.opts.backingStore.setPowerLevelContent(roomId, eventContent);

From 9443ddeb09ce7ff00490fc6b7369385e5cabee8f Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Thu, 6 Aug 2020 15:15:45 +0200
Subject: [PATCH 10/30] Add newsfile

---
 changelog.d/185.misc | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/185.misc

diff --git a/changelog.d/185.misc b/changelog.d/185.misc
new file mode 100644
index 00000000..fd713d1f
--- /dev/null
+++ b/changelog.d/185.misc
@@ -0,0 +1 @@
+Convert intent.js to TypeScript
\ No newline at end of file

From 2bc5f426f1ad5c029c0652d9e4f167dbcd311147 Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Fri, 7 Aug 2020 13:39:35 +0200
Subject: [PATCH 11/30] Remove Bluebird from intent.ts

---
 src/components/intent.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index 992c9626..dcdc30d7 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -13,7 +13,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import Bluebird from "bluebird";
 import MatrixUser from "../models/users/matrix";
 import JsSdk from "matrix-js-sdk";
 // eslint-disable-next-line @typescript-eslint/no-explicit-any

From fa52213703612b95f704482090cbd38be8a8beba Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Fri, 7 Aug 2020 14:16:46 +0200
Subject: [PATCH 12/30] Change export of intent in index.ts

---
 src/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/index.ts b/src/index.ts
index 05ce7b01..2653d6ad 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -16,7 +16,7 @@ limitations under the License.
 /* eslint-disable @typescript-eslint/no-var-requires */
 
 module.exports.ClientFactory = require("./components/client-factory");
-module.exports.Intent = require("./components/intent");
+export * from "./components/intent";
 module.exports.AppServiceBot = require("./components/app-service-bot");
 module.exports.StateLookup = require("./components/state-lookup").StateLookup;
 

From cf80380096ee7ba9c04570f9e22d400c24e182a9 Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Fri, 7 Aug 2020 14:25:09 +0200
Subject: [PATCH 13/30] Add newsfile

---
 changelog.d/190.misc | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/190.misc

diff --git a/changelog.d/190.misc b/changelog.d/190.misc
new file mode 100644
index 00000000..70ec3f6f
--- /dev/null
+++ b/changelog.d/190.misc
@@ -0,0 +1 @@
+Remove some bluebird imports and use async/await in some tests
\ No newline at end of file

From 979cf9daa070df202bcb7246f0b56efd862ebd27 Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Fri, 7 Aug 2020 14:26:21 +0200
Subject: [PATCH 14/30] Revert "Add newsfile"

This reverts commit cf80380096ee7ba9c04570f9e22d400c24e182a9.
---
 changelog.d/190.misc | 1 -
 1 file changed, 1 deletion(-)
 delete mode 100644 changelog.d/190.misc

diff --git a/changelog.d/190.misc b/changelog.d/190.misc
deleted file mode 100644
index 70ec3f6f..00000000
--- a/changelog.d/190.misc
+++ /dev/null
@@ -1 +0,0 @@
-Remove some bluebird imports and use async/await in some tests
\ No newline at end of file

From 17c39068e3779d1bb79e0b1cefab5509151c64dd Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Fri, 7 Aug 2020 15:38:30 +0200
Subject: [PATCH 15/30] Fix export of Intent

---
 src/components/intent.ts | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index dcdc30d7..bce458a6 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -92,7 +92,7 @@ type PowerLevelContent = {
     }
 };
 
-class Intent {
+export class Intent {
     private _requestCaches: {
         profile: ClientRequestCache,
         roomstate: ClientRequestCache,
@@ -851,5 +851,3 @@ class Intent {
         }
     }
 }
-
-module.exports = Intent;

From acaa0c653ce9a443f2e9920580638b8da3f21dc4 Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Fri, 7 Aug 2020 19:21:06 +0200
Subject: [PATCH 16/30] Fix import in bridge.js

---
 src/bridge.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/bridge.js b/src/bridge.js
index 2b380e10..e6a03299 100644
--- a/src/bridge.js
+++ b/src/bridge.js
@@ -27,7 +27,7 @@ const BridgeContext = require("./components/bridge-context");
 const ClientFactory = require("./components/client-factory");
 const AppServiceBot = require("./components/app-service-bot");
 const RequestFactory = require("./components/request-factory").RequestFactory;
-const Intent = require("./components/intent");
+const Intent = require("./components/intent").Intent;
 const RoomBridgeStore = require("./components/room-bridge-store");
 const UserBridgeStore = require("./components/user-bridge-store");
 const EventBridgeStore = require("./components/event-bridge-store");

From 356cb20e9b7a5c90203c3df767bcf7c811ed8425 Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Fri, 7 Aug 2020 21:10:58 +0200
Subject: [PATCH 17/30] Ingore changes to opts; await promise in _joinGuard

---
 spec/unit/intent.spec.js | 44 +++++++++++++++++++++++-----------------
 src/components/intent.ts | 13 +++++-------
 2 files changed, 30 insertions(+), 27 deletions(-)

diff --git a/spec/unit/intent.spec.js b/spec/unit/intent.spec.js
index 8e591829..be76b737 100644
--- a/spec/unit/intent.spec.js
+++ b/spec/unit/intent.spec.js
@@ -1,21 +1,21 @@
 "use strict";
-var Intent = require("../..").Intent;
-var log = require("../log");
+const Intent = require("../..").Intent;
+const log = require("../log");
 
 describe("Intent", function() {
-    var intent, client, botClient;
-    var userId = "@alice:bar";
-    var botUserId = "@bot:user";
-    var roomId = "!foo:bar";
+    let intent, client, botClient;
+    const userId = "@alice:bar";
+    const botUserId = "@bot:user";
+    const roomId = "!foo:bar";
+    const alreadyRegistered = {
+        registered: true
+    };
 
     beforeEach(
     /** @this */
     function() {
         log.beforeEach(this);
-        var alreadyRegistered = {
-            registered: true
-        };
-        var clientFields = [
+        const clientFields = [
             "credentials", "joinRoom", "invite", "leave", "ban", "unban",
             "kick", "getStateEvent", "setPowerLevel", "sendTyping", "sendEvent",
             "sendStateEvent", "setDisplayName", "setAvatarUrl",
@@ -150,9 +150,9 @@ describe("Intent", function() {
     });
 
     describe("sending state events", function() {
-        var validPowerLevels, invalidPowerLevels;
+        let validPowerLevels, invalidPowerLevels;
 
-        beforeEach(function() {
+        beforeEach(() => {
             // not interested in joins, so no-op them.
             intent.onEvent({
                 event_id: "test",
@@ -164,7 +164,7 @@ describe("Intent", function() {
                 }
             });
 
-            var basePowerLevelEvent = {
+            const basePowerLevelEvent = {
                 content: {
                     "ban": 50,
                     "events": {
@@ -259,13 +259,16 @@ describe("Intent", function() {
     });
 
     describe("sending message events", function() {
-        var content = {
+        const content = {
             body: "hello world",
             msgtype: "m.text",
         };
 
         beforeEach(function() {
-            intent.opts.dontCheckPowerLevel = true;
+            intent = new Intent(client, botClient, {
+                ...alreadyRegistered,
+                dontCheckPowerLevel: true,
+            });
             // not interested in joins, so no-op them.
             intent.onEvent({
                 event_id: "test",
@@ -304,7 +307,7 @@ describe("Intent", function() {
         });
 
         it("should try to join the room on M_FORBIDDEN then resend", function() {
-            var isJoined = false;
+            let isJoined = false;
             client.sendEvent.and.callFake(function() {
                 if (isJoined) {
                     return Promise.resolve({
@@ -350,7 +353,7 @@ describe("Intent", function() {
         });
 
         it("should fail if the resend after M_FORBIDDEN fails", function() {
-            var isJoined = false;
+            let isJoined = false;
             client.sendEvent.and.callFake(function() {
                 if (isJoined) {
                     return Promise.reject({
@@ -380,10 +383,13 @@ describe("Intent", function() {
 
     describe("signaling bridge error", function() {
         const reason = "m.event_not_handled"
-        var affectedUsers, eventId, bridge;
+        let affectedUsers, eventId, bridge;
 
         beforeEach(function() {
-            intent.opts.dontCheckPowerLevel = true;
+            intent = new Intent(client, botClient, {
+                ...alreadyRegistered,
+                dontCheckPowerLevel: true,
+            });
             // not interested in joins, so no-op them.
             intent.onEvent({
                 event_id: "test",
diff --git a/src/components/intent.ts b/src/components/intent.ts
index bce458a6..afa58db2 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -115,8 +115,8 @@ export class Intent {
         registered?: boolean;
     }
     // These two are only used if no opts.backingStore is provided to the constructor.
-    private _membershipStates: Record<string, MembershipState> = {};
-    private _powerLevels: Record<string, PowerLevelContent> = {};
+    private readonly _membershipStates: Record<string, MembershipState> = {};
+    private readonly _powerLevels: Record<string, PowerLevelContent> = {};
 
     /**
     * Create an entity which can fulfil the intent of a given user.
@@ -164,10 +164,6 @@ export class Intent {
     * @param opts.caching.size How many entries should be kept in the cache, before the oldest is dropped.
     */
     constructor(private client: MatrixClient, private botClient: MatrixClient, opts: IntentOpts = {}) {
-        opts = opts || {};
-
-        opts.enablePresence = opts.enablePresence !== false;
-
         if (opts.backingStore) {
             if (!opts.backingStore.setPowerLevelContent ||
                     !opts.backingStore.getPowerLevelContent ||
@@ -679,7 +675,8 @@ export class Intent {
     // in the room. If the promise rejects, join the room and retry the function.
     private async _joinGuard(roomId: string, promiseFn: () => Promise<unknown>) {
         try {
-            return promiseFn();
+            // await so we can handle the error
+            return await promiseFn();
         }
         catch (err) {
             if (err.errcode !== "M_FORBIDDEN") {
@@ -694,7 +691,7 @@ export class Intent {
     private async _ensureJoined(
         roomId: string, ignoreCache = false, viaServers?: string[], passthroughError = false
     ) {
-        const userId = this.client.credentials.userId;
+        const { userId } = this.client.credentials;
         const opts: { syncRoom: boolean, viaServers?: string[] } = {
             syncRoom: false,
         };

From e10ef1336fa58cbf20268dc52e75ef36371f1d5f Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Fri, 7 Aug 2020 21:18:23 +0200
Subject: [PATCH 18/30] Clone opts.backingStore

---
 src/components/intent.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index afa58db2..db5ca4b2 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -174,7 +174,7 @@ export class Intent {
         }
         this.opts = {
             ...opts,
-            backingStore: opts.backingStore || {
+            backingStore: opts.backingStore ? { ...opts.backingStore } : {
                 getMembership: (roomId: string, userId: string) => {
                     if (userId !== this.client.credentials.userId) {
                         return null;

From ebe8128a21b0fa6a8eaa079edd97973ea459e474 Mon Sep 17 00:00:00 2001
From: Will Hunt <will@half-shot.uk>
Date: Mon, 10 Aug 2020 13:09:41 +0100
Subject: [PATCH 19/30] Bump matrix-js-sdk to 8.0.1

---
 package-lock.json | 18 +++++++++---------
 package.json      |  2 +-
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index a7846c1f..4a8a5153 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -236,9 +236,9 @@
       "dev": true
     },
     "@babel/runtime": {
-      "version": "7.10.3",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz",
-      "integrity": "sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==",
+      "version": "7.11.2",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz",
+      "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==",
       "requires": {
         "regenerator-runtime": "^0.13.4"
       }
@@ -2772,9 +2772,9 @@
       }
     },
     "matrix-js-sdk": {
-      "version": "6.2.2",
-      "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-6.2.2.tgz",
-      "integrity": "sha512-uTXmKVzl7GXM3R70cl2E87H+mtwN3ILqPeB80Z/2ITN9Vaf9pMURCCAuHePEBXbhnD7DOZj7Cke42uP+ByR6Hw==",
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-8.0.1.tgz",
+      "integrity": "sha512-DT2YjWi8l2eHyNTKZOhBkN/EakIMDDEglSCg+RWY4QzFaXYlSwfwfzzCujjtq1hxVSKim8NC7KqxgNetOiBegA==",
       "requires": {
         "@babel/runtime": "^7.8.3",
         "another-json": "^0.2.0",
@@ -3412,9 +3412,9 @@
       }
     },
     "regenerator-runtime": {
-      "version": "0.13.5",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
-      "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
+      "version": "0.13.7",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
+      "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
     },
     "regex-not": {
       "version": "1.0.2",
diff --git a/package.json b/package.json
index 3eeb0743..476e72f7 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
     "is-my-json-valid": "^2.20.5",
     "js-yaml": "^3.14.0",
     "matrix-appservice": "^0.4.2",
-    "matrix-js-sdk": "^6.0.0",
+    "matrix-js-sdk": "^8.0.1",
     "nedb": "^1.8.0",
     "nopt": "^4.0.3",
     "p-queue": "^6.6.0",

From 32800979bf2af130c9958c0abaa2c430b5810812 Mon Sep 17 00:00:00 2001
From: Will Hunt <will@half-shot.uk>
Date: Mon, 10 Aug 2020 13:10:21 +0100
Subject: [PATCH 20/30] changelog

---
 changelog.d/194.feature | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/194.feature

diff --git a/changelog.d/194.feature b/changelog.d/194.feature
new file mode 100644
index 00000000..331e3765
--- /dev/null
+++ b/changelog.d/194.feature
@@ -0,0 +1 @@
+Bump matrix-js-sdk to 8.0.1
\ No newline at end of file

From 01e0aa10c9d964c0acf4955e471346dc14e09824 Mon Sep 17 00:00:00 2001
From: Will Hunt <will@half-shot.uk>
Date: Mon, 10 Aug 2020 15:37:40 +0100
Subject: [PATCH 21/30] Fix intent parts, make client any again

---
 src/components/intent.ts | 40 +++++++---------------------------------
 1 file changed, 7 insertions(+), 33 deletions(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index db5ca4b2..5d77ac02 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -15,38 +15,12 @@ limitations under the License.
 
 import MatrixUser from "../models/users/matrix";
 import JsSdk from "matrix-js-sdk";
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 const { MatrixEvent, RoomMember } = JsSdk as any;
 import ClientRequestCache from "./client-request-cache";
 import { defer } from "../utils/promiseutil";
 
-type MatrixClient = {
-    credentials: {
-        userId: string;
-    },
-    ban: (roomId: string, target: string, reason: string) => Promise<unknown>;
-    createAlias: (alias: string, roomId: string) => Promise<unknown>;
-    createRoom: (opts: Record<string, unknown>) => Promise<unknown>;
-    fetchRoomEvent: (roomId: string, eventId: string) => Promise<unknown>;
-    getStateEvent: (roomId: string, eventType: string, stateKey: string) => Promise<unknown>;
-    invite: (roomId: string, userId: string) => Promise<unknown>;
-    joinRoom: (roomId: string, opts: Record<string, unknown>) => Promise<unknown>;
-    kick: (roomId: string, target: string, reason: string) => Promise<unknown>;
-    leave: (roomId: string) => Promise<unknown>;
-    register: (localpart: string) => Promise<unknown>;
-    roomState: (roomId: string) => Promise<unknown>;
-    sendEvent: (roomId: string, type: string, content: Record<string, unknown>) => Promise<unknown>;
-    sendReadReceipt: (event: unknown) => Promise<unknown>;
-    sendStateEvent: (roomId: string, type: string, content: Record<string, unknown>, skey: string) => Promise<unknown>;
-    sendTyping: (roomId: string, isTyping: boolean) => Promise<unknown>;
-    setAvatarUrl: (url: string) => Promise<unknown>;
-    setDisplayName: (name: string) => Promise<unknown>;
-    setPowerLevel: (roomId: string, target: string, level: number, event: unknown) => Promise<unknown>;
-    // eslint-disable-next-line camelcase
-    setPresence: (presence: { presence: string, status_msg?: string }) => Promise<unknown>;
-    getProfileInfo: (userId: string, info: string) => Promise<unknown>;
-    unban: (roomId: string, target: string) => Promise<unknown>;
-};
 
 type BridgeErrorReason = "m.event_not_handled" | "m.event_too_old"
     | "m.internal_error" | "m.foreign_network_error" | "m.event_unknown";
@@ -163,7 +137,7 @@ export class Intent {
     * @param opts.caching.ttl How long requests can stay in the cache, in milliseconds.
     * @param opts.caching.size How many entries should be kept in the cache, before the oldest is dropped.
     */
-    constructor(private client: MatrixClient, private botClient: MatrixClient, opts: IntentOpts = {}) {
+    constructor(public readonly client: any, private readonly botClient: any, opts: IntentOpts = {}) {
         if (opts.backingStore) {
             if (!opts.backingStore.setPowerLevelContent ||
                     !opts.backingStore.getPowerLevelContent ||
@@ -326,9 +300,9 @@ export class Intent {
      * Set the power level of the given target.
      * @param roomId The room to set the power level in.
      * @param target The target user ID
-     * @param level The desired level
+     * @param level The desired level. Undefined will remove the users custom power level.
      */
-    public async setPowerLevel(roomId: string, target: string, level: number) {
+    public async setPowerLevel(roomId: string, target: string, level: number|undefined) {
         await this._ensureJoined(roomId);
         const event = await this._ensureHasPowerLevelFor(roomId, "m.room.power_levels");
         return this.client.setPowerLevel(roomId, target, level, event);
@@ -471,7 +445,7 @@ export class Intent {
      * @param reason Optional. The reason for the kick.
      * @return Resolved when kickked, else rejected with an error.
      */
-    public async kick(roomId: string, target: string, reason: string) {
+    public async kick(roomId: string, target: string, reason?: string) {
         await this._ensureJoined(roomId);
         return this.client.kick(roomId, target, reason);
     }
@@ -485,7 +459,7 @@ export class Intent {
      * @param reason Optional. The reason for the ban.
      * @return Resolved when banned, else rejected with an error.
      */
-    public async ban(roomId: string, target: string, reason: string) {
+    public async ban(roomId: string, target: string, reason?: string) {
         await this._ensureJoined(roomId);
         return this.client.ban(roomId, target, reason);
     }
@@ -511,7 +485,7 @@ export class Intent {
      * @param viaServers The server names to try and join through in
      * addition to those that are automatically chosen.
      */
-    public async join(roomId: string, viaServers: string[]) {
+    public async join(roomId: string, viaServers?: string[]) {
         await this._ensureJoined(roomId, false, viaServers);
     }
 

From 66a37bd6291546c1030855d1f5e9a8d97f2fc010 Mon Sep 17 00:00:00 2001
From: Will Hunt <will@half-shot.uk>
Date: Mon, 10 Aug 2020 15:37:55 +0100
Subject: [PATCH 22/30] Request factory should not be generic, but newRequest
 should

---
 src/components/request-factory.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/request-factory.ts b/src/components/request-factory.ts
index 40cdda33..676d45d1 100644
--- a/src/components/request-factory.ts
+++ b/src/components/request-factory.ts
@@ -22,7 +22,7 @@ type TimeoutFunction = (req: Request<unknown>) => void;
  * A factory which can create {@link Request} objects. Useful for
  * adding "default" handlers to requests.
  */
-export class RequestFactory<T> {
+export class RequestFactory {
     private _resolves: HandlerFunction[] = [];
     private _rejects: HandlerFunction[] = [];
     private _timeouts: {fn: TimeoutFunction, timeout: number}[] = [];
@@ -33,8 +33,8 @@ export class RequestFactory<T> {
      * @param opts The options to pass to the Request constructor, if any.
      * @return A new request object
      */
-    public newRequest(opts: RequestOpts<T>) {
-        const req = new Request(opts);
+    public newRequest<T>(opts?: RequestOpts<T>) {
+        const req = new Request(opts || {data: null});
         req.getPromise().then((res) => {
             this._resolves.forEach((resolveFn) => {
                 resolveFn(req, res);

From 06d59ef123b0e6dc660bc6431b61a70a8b342c8e Mon Sep 17 00:00:00 2001
From: Will Hunt <will@half-shot.uk>
Date: Mon, 10 Aug 2020 15:38:14 +0100
Subject: [PATCH 23/30] expose StateLookupEvent, make some parameters optional

---
 src/components/state-lookup.ts | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/components/state-lookup.ts b/src/components/state-lookup.ts
index 7f604240..37784aa3 100644
--- a/src/components/state-lookup.ts
+++ b/src/components/state-lookup.ts
@@ -18,7 +18,7 @@ import PQueue from "p-queue";
 interface StateLookupOpts {
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     client: any; //TODO: Needs to be MatrixClient (once that becomes TypeScript)
-    stateLookupConcurrency: number;
+    stateLookupConcurrency?: number;
     eventTypes?: string[];
     retryStateInMs?: number;
 }
@@ -33,7 +33,7 @@ interface StateLookupRoom {
     };
 }
 
-interface StateLookupEvent {
+export interface StateLookupEvent {
     // eslint-disable-next-line camelcase
     room_id: string;
     // eslint-disable-next-line camelcase
@@ -41,6 +41,7 @@ interface StateLookupEvent {
     type: string;
     // eslint-disable-next-line camelcase
     event_id: string;
+    content: unknown;
 }
 
 const RETRY_STATE_IN_MS = 300;
@@ -96,7 +97,7 @@ export class StateLookup {
      * array of events, which may be empty.
      * @return {?Object|Object[]}
      */
-    public getState(roomId: string, eventType: string, stateKey?: string): unknown|unknown[] {
+    public getState(roomId: string, eventType: string, stateKey?: string): null|StateLookupEvent|StateLookupEvent[] {
         const r = this.dict[roomId];
         if (!r) {
             return stateKey === undefined ? [] : null;

From 596e0ea7625cf0739f855daedcc8490c368438da Mon Sep 17 00:00:00 2001
From: Will Hunt <will@half-shot.uk>
Date: Mon, 10 Aug 2020 15:38:34 +0100
Subject: [PATCH 24/30] Fix exports, publish declarations

---
 src/index.ts  | 4 ++--
 tsconfig.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/index.ts b/src/index.ts
index 8c2c1664..88ac7c5c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -19,11 +19,11 @@ export * from "./components/request-factory";
 
 export * from "./components/client-factory";
 export * from "./components/intent";
+export * from "./components/state-lookup";
 
 /* eslint-disable @typescript-eslint/no-var-requires */
 
 module.exports.AppServiceBot = require("./components/app-service-bot");
-module.exports.StateLookup = require("./components/state-lookup").StateLookup;
 
 // Config and CLI
 module.exports.Cli = require("./components/cli");
@@ -50,7 +50,7 @@ module.exports.AppServiceRegistration = (
 
 const jsSdk = require("matrix-js-sdk");
 
-module.exports.ContentRepo = {
+export const ContentRepo = {
     getHttpUriForMxc: jsSdk.getHttpUriForMxc,
     getIdenticonUri: jsSdk.getIdenticonUri,
 }
diff --git a/tsconfig.json b/tsconfig.json
index 68621ecf..6a59f0b2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,7 @@
     "module": "commonjs",
     "allowJs": true,
     "checkJs": false,
-    "declaration": false,
+    "declaration": true,
     "sourceMap": true,
     "outDir": "./lib",
     "composite": false,

From e027f071835160d106616950564bf8d787d3531f Mon Sep 17 00:00:00 2001
From: Will Hunt <will@half-shot.uk>
Date: Mon, 10 Aug 2020 16:02:28 +0100
Subject: [PATCH 25/30] Validate event before insertion

---
 src/components/state-lookup.ts | 26 +++++++++++++++++---------
 1 file changed, 17 insertions(+), 9 deletions(-)

diff --git a/src/components/state-lookup.ts b/src/components/state-lookup.ts
index 37784aa3..99a65817 100644
--- a/src/components/state-lookup.ts
+++ b/src/components/state-lookup.ts
@@ -122,12 +122,7 @@ export class StateLookup {
                 () => this._client.roomState(roomId)
             );
             events.forEach((ev) => {
-                if (this.eventTypes[ev.type]) {
-                    if (!r.events[ev.type]) {
-                        r.events[ev.type] = {};
-                    }
-                    r.events[ev.type][ev.state_key] = ev;
-                }
+                this.insertEvent(r, ev);
             });
             return r;
         }
@@ -192,9 +187,22 @@ export class StateLookup {
         }
 
         // blunt update
-        if (!r.events[event.type]) {
-            r.events[event.type] = {};
+        this.insertEvent(r, event);
+    }
+
+    private insertEvent(roomSet: StateLookupRoom, event: StateLookupEvent) {
+        if (typeof event.content !== "object") {
+            // Reject - unexpected content type
+            return;
+        }
+        if (!event.type || !event.state_key) {
+            // Reject - missing keys
+            return;
+        }
+        // blunt update
+        if (!roomSet.events[event.type]) {
+            roomSet.events[event.type] = {};
         }
-        r.events[event.type][event.state_key] = event;
+        roomSet.events[event.type][event.state_key] = event;
     }
 }

From 12354a69579e16916887c765821e1e771111f7e2 Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Mon, 10 Aug 2020 19:55:10 +0200
Subject: [PATCH 26/30] Don't assume the type of eventContent

---
 src/components/intent.ts | 162 +++++++++++++++++++++------------------
 1 file changed, 88 insertions(+), 74 deletions(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index db5ca4b2..8239f3e1 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -53,13 +53,15 @@ type BridgeErrorReason = "m.event_not_handled" | "m.event_too_old"
 
 type MembershipState = "join" | "invite" | "leave" | null; // null = unknown
 
+type BackingStore = {
+    getMembership: (roomId: string, userId: string) => MembershipState,
+    getPowerLevelContent: (roomId: string) => Record<string, unknown> | undefined,
+    setMembership: (roomId: string, userId: string, membership: MembershipState) => void,
+    setPowerLevelContent: (roomId: string, content: Record<string, unknown>) => void,
+};
+
 interface IntentOpts {
-    backingStore?: {
-        getMembership: (roomId: string, userId: string) => MembershipState,
-        getPowerLevelContent: (roomId: string) => PowerLevelContent,
-        setMembership: (roomId: string, userId: string, membership: MembershipState) => void,
-        setPowerLevelContent: (roomId: string, content: PowerLevelContent) => void,
-    },
+    backingStore?: BackingStore,
     caching?: {
         ttl?: number,
         size?: number,
@@ -70,6 +72,18 @@ interface IntentOpts {
     registered?: boolean;
 }
 
+/**
+ * Returns the first parameter that is a number or 0.
+ */
+const returnFirstNumber = (...args: unknown[]) => {
+    for (const arg of args) {
+        if (typeof arg === "number") {
+            return arg;
+        }
+    }
+    return 0;
+}
+
 const STATE_EVENT_TYPES = [
     "m.room.name", "m.room.topic", "m.room.power_levels", "m.room.member",
     "m.room.join_rules", "m.room.history_visibility"
@@ -79,16 +93,16 @@ const DEFAULT_CACHE_SIZE = 1024;
 
 type PowerLevelContent = {
     // eslint-disable-next-line camelcase
-    state_default: number;
+    state_default?: unknown;
     // eslint-disable-next-line camelcase
-    events_default: number;
+    events_default?: unknown;
     // eslint-disable-next-line camelcase
-    users_default: number;
-    users: {
-        [userId: string]: number;
+    users_default?: unknown;
+    users?: {
+        [userId: string]: unknown;
     },
-    events: {
-        [eventType: string]: number;
+    events?: {
+        [eventType: string]: unknown;
     }
 };
 
@@ -99,12 +113,7 @@ export class Intent {
         event: ClientRequestCache
     }
     private opts: {
-        backingStore: {
-            getMembership: (roomId: string, userId: string) => MembershipState,
-            getPowerLevelContent: (roomId: string) => PowerLevelContent,
-            setMembership: (roomId: string, userId: string, membership: MembershipState) => void,
-            setPowerLevelContent: (roomId: string, content: PowerLevelContent) => void,
-        },
+        backingStore: BackingStore,
         caching: {
             ttl: number,
             size: number,
@@ -190,7 +199,7 @@ export class Intent {
                     }
                     this._membershipStates[roomId] = membership;
                 },
-                setPowerLevelContent: (roomId: string, content: PowerLevelContent) => {
+                setPowerLevelContent: (roomId: string, content: Object) => {
                     this._powerLevels[roomId] = content;
                 },
             },
@@ -768,64 +777,69 @@ export class Intent {
 
     private async _ensureHasPowerLevelFor(roomId: string, eventType: string) {
         if (this.opts.dontCheckPowerLevel && eventType !== "m.room.power_levels") {
-            return Promise.resolve();
+            return;
         }
         const userId = this.client.credentials.userId;
-        const plContent = this.opts.backingStore.getPowerLevelContent(roomId);
-        let promise = Promise.resolve(plContent);
-        if (!plContent) {
-            promise = this.client.getStateEvent(roomId, "m.room.power_levels", "") as Promise<PowerLevelContent>;
+        let plContent = this.opts.backingStore.getPowerLevelContent(roomId)
+            || await this.client.getStateEvent(roomId, "m.room.power_levels", "");
+        const eventContent: PowerLevelContent = plContent && typeof plContent === "object" ? plContent : {};
+        this.opts.backingStore.setPowerLevelContent(roomId, eventContent);
+        const event = {
+            content: typeof eventContent === "object" ? eventContent : {},
+            room_id: roomId,
+            sender: "",
+            event_id: "_",
+            state_key: "",
+            type: "m.room.power_levels"
         }
-        return promise.then((eventContent) => {
-            this.opts.backingStore.setPowerLevelContent(roomId, eventContent);
-            const event = {
-                content: eventContent,
-                room_id: roomId,
-                sender: "",
-                event_id: "_",
-                state_key: "",
-                type: "m.room.power_levels"
-            }
-            const powerLevelEvent = new MatrixEvent(event);
-            // What level do we need for this event type?
-            const defaultLevel = STATE_EVENT_TYPES.includes(eventType)
-                ? event.content.state_default
-                : event.content.events_default;
-            const requiredLevel = event.content.events[eventType] || defaultLevel;
-
-            // Parse out what level the client has by abusing the JS SDK
-            const roomMember = new RoomMember(roomId, userId);
-            roomMember.setPowerLevelEvent(powerLevelEvent);
-
-            if (requiredLevel > roomMember.powerLevel) {
-                // can the bot update our power level?
-                const bot = new RoomMember(roomId, this.botClient.credentials.userId);
-                bot.setPowerLevelEvent(powerLevelEvent);
-                const levelRequiredToModifyPowerLevels = event.content.events[
-                    "m.room.power_levels"
-                ] || event.content.state_default;
-                if (levelRequiredToModifyPowerLevels > bot.powerLevel) {
-                    // even the bot has no power here.. give up.
-                    throw new Error(
-                        "Cannot ensure client has power level for event " + eventType +
-                        " : client has " + roomMember.powerLevel + " and we require " +
-                        requiredLevel + " and the bot doesn't have permission to " +
-                        "edit the client's power level."
-                    );
-                }
-                // update the client's power level first
-                return this.botClient.setPowerLevel(
-                    roomId, userId, requiredLevel, powerLevelEvent
-                ).then(() => {
-                    // tweak the level for the client to reflect the new reality
-                    const userLevels = powerLevelEvent.getContent().users || {};
-                    userLevels[userId] = requiredLevel;
-                    powerLevelEvent.getContent().users = userLevels;
-                    return Promise.resolve(powerLevelEvent);
-                });
+        const powerLevelEvent = new MatrixEvent(event);
+        // What level do we need for this event type?
+        const defaultLevel = STATE_EVENT_TYPES.includes(eventType)
+            ? event.content.state_default
+            : event.content.events_default;
+        const requiredLevel = returnFirstNumber(
+            // If these are invalid or not provided, default to 0 according to the Spec.
+            // https://matrix.org/docs/spec/client_server/r0.6.0#m-room-power-levels
+            (event.content.events && event.content.events[eventType]),
+            defaultLevel,
+            0
+        );
+
+
+        // Parse out what level the client has by abusing the JS SDK
+        const roomMember = new RoomMember(roomId, userId);
+        roomMember.setPowerLevelEvent(powerLevelEvent);
+
+        if (requiredLevel > roomMember.powerLevel) {
+            // can the bot update our power level?
+            const bot = new RoomMember(roomId, this.botClient.credentials.userId);
+            bot.setPowerLevelEvent(powerLevelEvent);
+            const levelRequiredToModifyPowerLevels = returnFirstNumber(
+                // If these are invalid or not provided, default to 0 according to the Spec.
+                // https://matrix.org/docs/spec/client_server/r0.6.0#m-room-power-levels
+                event.content.events && event.content.events["m.room.power_levels"],
+                event.content.state_default,
+                0
+            );
+            if (levelRequiredToModifyPowerLevels > bot.powerLevel) {
+                // even the bot has no power here.. give up.
+                throw new Error(
+                    "Cannot ensure client has power level for event " + eventType +
+                    " : client has " + roomMember.powerLevel + " and we require " +
+                    requiredLevel + " and the bot doesn't have permission to " +
+                    "edit the client's power level."
+                );
             }
-            return Promise.resolve(powerLevelEvent);
-        });
+            // update the client's power level first
+            await this.botClient.setPowerLevel(
+                roomId, userId, requiredLevel, powerLevelEvent
+            );
+            // tweak the level for the client to reflect the new reality
+            const userLevels = powerLevelEvent.getContent().users || {};
+            userLevels[userId] = requiredLevel;
+            powerLevelEvent.getContent().users = userLevels;
+        }
+        return powerLevelEvent;
     }
 
     private async _ensureRegistered() {

From 9a756f32fb3b6582e8d23613621316e362a63d3e Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Mon, 10 Aug 2020 19:56:44 +0200
Subject: [PATCH 27/30] Prefer Record over Object

---
 src/components/intent.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index 8239f3e1..03778c63 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -199,7 +199,7 @@ export class Intent {
                     }
                     this._membershipStates[roomId] = membership;
                 },
-                setPowerLevelContent: (roomId: string, content: Object) => {
+                setPowerLevelContent: (roomId: string, content: Record<string, unknown>) => {
                     this._powerLevels[roomId] = content;
                 },
             },

From 5739e6561b33593a39994ebf8fb6cb7646e8d4f6 Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Mon, 10 Aug 2020 20:19:48 +0200
Subject: [PATCH 28/30] Fix ESLint errors

---
 src/components/intent.ts | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/src/components/intent.ts b/src/components/intent.ts
index 821bc390..3c5a4af9 100644
--- a/src/components/intent.ts
+++ b/src/components/intent.ts
@@ -749,12 +749,18 @@ export class Intent {
         return deferredPromise.promise;
     }
 
+    /**
+     * Ensures that the client has the required power level to post the event type.
+     * @param roomId Required as power levels exist inside a room.
+     * @param eventTypes The event type to check the permissions for.
+     * @return If found, the power level event
+     */
     private async _ensureHasPowerLevelFor(roomId: string, eventType: string) {
         if (this.opts.dontCheckPowerLevel && eventType !== "m.room.power_levels") {
-            return;
+            return undefined;
         }
         const userId = this.client.credentials.userId;
-        let plContent = this.opts.backingStore.getPowerLevelContent(roomId)
+        const plContent = this.opts.backingStore.getPowerLevelContent(roomId)
             || await this.client.getStateEvent(roomId, "m.room.power_levels", "");
         const eventContent: PowerLevelContent = plContent && typeof plContent === "object" ? plContent : {};
         this.opts.backingStore.setPowerLevelContent(roomId, eventContent);

From deb2f38ab33b74567bba6be879440da1d26f5a4b Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Mon, 10 Aug 2020 20:54:53 +0200
Subject: [PATCH 29/30] Test for type and state_key being strings, not being
 truthy

---
 src/components/state-lookup.ts | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/components/state-lookup.ts b/src/components/state-lookup.ts
index 99a65817..19f41248 100644
--- a/src/components/state-lookup.ts
+++ b/src/components/state-lookup.ts
@@ -110,9 +110,7 @@ export class StateLookup {
             return es[eventType][stateKey] || null;
         }
 
-        return Object.keys(es[eventType]).map(function(skey) {
-            return es[eventType][skey];
-        });
+        return Object.keys(es[eventType]).map(skey => es[eventType][skey]);
     }
 
     private async getInitialState(roomId: string): Promise<StateLookupRoom> {
@@ -195,8 +193,8 @@ export class StateLookup {
             // Reject - unexpected content type
             return;
         }
-        if (!event.type || !event.state_key) {
-            // Reject - missing keys
+        if (typeof event.type !== "string" || typeof event.state_key !== "string") {
+            // Reject - invalid keys
             return;
         }
         // blunt update

From 198bf8938f3b00d15337b68e98ba45d67f75da8f Mon Sep 17 00:00:00 2001
From: Christian Paul <christianp@matrix.org>
Date: Mon, 10 Aug 2020 20:55:22 +0200
Subject: [PATCH 30/30] Refactor the syntax of state-lookup tests to use
 async/await

---
 spec/unit/state-lookup.spec.js | 96 ++++++++++++++++------------------
 1 file changed, 44 insertions(+), 52 deletions(-)

diff --git a/spec/unit/state-lookup.spec.js b/spec/unit/state-lookup.spec.js
index 915ddd4c..aed6de1b 100644
--- a/spec/unit/state-lookup.spec.js
+++ b/spec/unit/state-lookup.spec.js
@@ -18,34 +18,31 @@ describe("StateLookup", function() {
         });
     });
 
-    describe("trackRoom", function() {
+    describe("trackRoom", () => {
         it("should return a Promise which is resolved after the HTTP call " +
-        "to /state returns", function(done) {
+        "to /state returns", async() => {
             var statePromise = createStatePromise([]);
             cli.roomState.and.returnValue(statePromise.promise);
             var p = lookup.trackRoom("!foo:bar");
             expect(p.isPending()).toBe(true); // not resolved HTTP call yet
-            Bluebird.delay(5).then(function() {
-                expect(p.isPending()).toBe(true); // still not resolved HTTP call
-                statePromise.resolve();
-                return p; // Should resolve now HTTP call is resolved
-            }).then(function() {
-                done();
-            });
+            await Bluebird.delay(5);
+            expect(p.isPending()).toBe(true); // still not resolved HTTP call
+            statePromise.resolve();
+            await p; // Should resolve now HTTP call is resolved
         });
 
         it("should return the same Promise if called multiple times with the " +
         "same room ID", function() {
-            var statePromise = createStatePromise([]);
+            const statePromise = createStatePromise([]);
             cli.roomState.and.returnValue(statePromise.promise);
-            var p = lookup.trackRoom("!foo:bar");
-            var q = lookup.trackRoom("!foo:bar");
+            const p = lookup.trackRoom("!foo:bar");
+            const q = lookup.trackRoom("!foo:bar");
             expect(p).toBe(q);
         });
 
-        it("should be able to have >1 in-flight track requests at once", function(done) {
-            var stateA = createStatePromise([]);
-            var stateB = createStatePromise([]);
+        it("should be able to have >1 in-flight track requests at once", async() => {
+            const stateA = createStatePromise([]);
+            const stateB = createStatePromise([]);
             cli.roomState.and.callFake(function(roomId) {
                 if (roomId === "!a:foobar") {
                     return stateA.promise;
@@ -55,16 +52,13 @@ describe("StateLookup", function() {
                 }
                 throw new Error("Unexpected room ID: " + roomId);
             });
-            var promiseA = lookup.trackRoom("!a:foobar");
-            var promiseB = lookup.trackRoom("!b:foobar");
+            const promiseA = lookup.trackRoom("!a:foobar");
+            const promiseB = lookup.trackRoom("!b:foobar");
             stateA.resolve();
-            promiseA.then(function() {
-                expect(promiseB.isPending()).toBe(true);
-                stateB.resolve();
-                return promiseB;
-            }).then(function() {
-                done();
-            });
+            await promiseA;
+            expect(promiseB.isPending()).toBe(true);
+            stateB.resolve();
+            await promiseB;
         });
 
         it("should retry the HTTP call on non 4xx, 5xx errors", async function() {
@@ -109,28 +103,28 @@ describe("StateLookup", function() {
         });
     });
 
-    describe("onEvent", function() {
-        it("should update the state lookup map", function(done) {
-            cli.roomState.and.callFake(function(roomId) {
-                return Promise.resolve([
-                    {type: "m.room.name", state_key: "", room_id: "!foo:bar",
-                        content: { name: "Foo" }}
-                ]);
+    describe("onEvent", () => {
+        it("should update the state lookup map", async() => {
+            cli.roomState.and.callFake(async(roomId) => {
+                return [{
+                    type: "m.room.name",
+                    state_key: "",
+                    room_id: "!foo:bar",
+                    content: { name: "Foo" },
+                }];
             });
 
-            lookup.trackRoom("!foo:bar").then(function() {
-                expect(
-                    lookup.getState("!foo:bar", "m.room.name", "").content.name
-                ).toEqual("Foo");
-                lookup.onEvent(
-                    {type: "m.room.name", state_key: "", room_id: "!foo:bar",
-                        content: { name: "Bar" }}
-                );
-                expect(
-                    lookup.getState("!foo:bar", "m.room.name", "").content.name
-                ).toEqual("Bar");
-                done();
-            });
+            await lookup.trackRoom("!foo:bar")
+            expect(
+                lookup.getState("!foo:bar", "m.room.name", "").content.name
+            ).toEqual("Foo");
+            lookup.onEvent(
+                {type: "m.room.name", state_key: "", room_id: "!foo:bar",
+                    content: { name: "Bar" }}
+            );
+            expect(
+                lookup.getState("!foo:bar", "m.room.name", "").content.name
+            ).toEqual("Bar");
         });
 
         it("should clobber events from in-flight track requests", async() => {
@@ -154,10 +148,10 @@ describe("StateLookup", function() {
         });
     });
 
-    describe("getState", function() {
-        beforeEach(function(done) {
-            cli.roomState.and.callFake(function(roomId) {
-                return Promise.resolve([
+    describe("getState", () => {
+        beforeEach(async() => {
+            cli.roomState.and.callFake(async(roomId) => {
+                return [
                     {type: "m.room.name", state_key: "", content: { name: "Foo" }},
                     {type: "m.room.topic", state_key: "", content: { name: "Bar" }},
                     {type: "m.room.member", state_key: "@alice:bar", content: {
@@ -168,12 +162,10 @@ describe("StateLookup", function() {
                         displayname: "Bob",
                         membership: "invite"
                     }},
-                ]);
+                ];
             });
 
-            lookup.trackRoom("!foo:bar").then(function() {
-                done();
-            });
+            await lookup.trackRoom("!foo:bar");
         });
 
         it("should return null for no match with state_key", function() {