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() {