From 4cbab723693625e25232c050cae19380bfb088fe Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 26 Oct 2015 15:27:44 +0000 Subject: [PATCH 1/4] Resolve invites to profile info This is so inviters/invitees have a display name and avatar_url if they have set one. This info isn't contained in the m.room.member event so we get it direct from /profile. This is gated behind `resolveInvitesToProfiles` on `startClient(opts)`. --- lib/client.js | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/lib/client.js b/lib/client.js index 576057d43..eef32f0f7 100644 --- a/lib/client.js +++ b/lib/client.js @@ -141,6 +141,7 @@ function MatrixClient(opts) { this.callList = { // callId: MatrixCall }; + this._config = {}; // see startClient() // try constructing a MatrixCall to see if we are running in an environment // which has WebRTC. If we are, listen for and handle m.call.* events. @@ -1948,6 +1949,9 @@ function doInitialSync(client, historyLen, includeArchived) { * @param {Number} opts.initialSyncLimit The event limit= to apply * to initial sync. Default: 8. * @param {Boolean} opts.includeArchivedRooms True to put archived=true + * @param {Boolean} opts.resolveInvitesToProfiles True to do /profile requests + * on every invite event if the displayname/avatar_url is not known for this user ID. + * Default: false. * on the /initialSync request. Default: false. */ MatrixClient.prototype.startClient = function(opts) { @@ -1965,6 +1969,8 @@ MatrixClient.prototype.startClient = function(opts) { opts = opts || {}; opts.initialSyncLimit = opts.initialSyncLimit || 8; opts.includeArchivedRooms = opts.includeArchivedRooms || false; + opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false; + this._config = opts; if (CRYPTO_ENABLED && this.sessionStore !== null) { this.uploadKeys(5); @@ -2019,6 +2025,7 @@ function _pollForEvents(client) { events = utils.map(data.chunk, _PojoToMatrixEventMapper(self)); } if (!(self.store instanceof StubStore)) { + var roomIdsWithNewInvites = {}; // bucket events based on room. var i = 0; var roomIdToEvents = {}; @@ -2030,6 +2037,10 @@ function _pollForEvents(client) { roomIdToEvents[roomId] = []; } roomIdToEvents[roomId].push(events[i]); + if (events[i].getType() === "m.room.member" && + events[i].getContent().membership === "invite") { + roomIdsWithNewInvites[roomId] = true; + } } else if (events[i].getType() === "m.presence") { var usr = self.store.getUser(events[i].getContent().user_id); @@ -2043,6 +2054,7 @@ function _pollForEvents(client) { } } } + // add events to room var roomIds = utils.keys(roomIdToEvents); utils.forEach(roomIds, function(roomId) { @@ -2077,6 +2089,10 @@ function _pollForEvents(client) { _syncRoom(self, room); } }); + + Object.keys(roomIdsWithNewInvites).forEach(function(inviteRoomId) { + _resolveInvites(self.store.getRoom(inviteRoomId)); + }); } if (data) { self.store.setSyncToken(data.end); @@ -2135,6 +2151,8 @@ function _processRoomEvents(client, room, stateEventList, messageChunk) { room.oldState.setStateEvents(oldStateEvents); room.currentState.setStateEvents(stateEvents); + _resolveInvites(client, room); + // add events to the timeline *after* setting the state // events so messages use the right display names. Initial sync // returns messages in chronological order, so we need to reverse @@ -2179,6 +2197,47 @@ function reEmit(reEmitEntity, emittableEntity, eventNames) { }); } +function _resolveInvites(client, room) { + if (!room || !client.resolveInvitesToProfiles) { + return; + } + // For each invited room member we want to give them a displayname/avatar url + // if they have one (the m.room.member invites don't contain this). + room.getMembersWithMembership("invite").forEach(function(member) { + if (member._requestedProfileInfo) { + return; + } + member._requestedProfileInfo = true; + // try to get a cached copy first. + var user = client.getUser(member.userId); + var promise; + if (user) { + promise = q({ + avatar_url: user.avatarUrl, + displayname: user.displayName + }); + } + else { + promise = client.getProfileInfo(member.userId); + } + promise.done(function(info) { + // slightly naughty by doctoring the invite event but this means all + // the code paths remain the same between invite/join display name stuff + // which is a worthy trade-off for some minor pollution. + var inviteEvent = member.events.member; + if (inviteEvent.getContent().membership !== "invite") { + // between resolving and now they have since joined, so don't clobber + return; + } + inviteEvent.getContent().avatar_url = info.avatar_url; + inviteEvent.getContent().displayname = info.displayname; + member.setMembershipEvent(inviteEvent, room.currentState); // fire listeners + }, function(err) { + // OH WELL. + }); + }); +} + function setupCallEventHandler(client) { var candidatesByCall = { // callId: [Candidate] From be6d64fbfd784072ffd1339c9c15feb720fedc9e Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 26 Oct 2015 16:12:06 +0000 Subject: [PATCH 2/4] Add integration tests; fix bugs. --- lib/client.js | 4 +- spec/integ/matrix-client-syncing.spec.js | 128 +++++++++++++++++++++-- spec/mock-request.js | 1 + 3 files changed, 125 insertions(+), 8 deletions(-) diff --git a/lib/client.js b/lib/client.js index eef32f0f7..fa65ef005 100644 --- a/lib/client.js +++ b/lib/client.js @@ -2091,7 +2091,7 @@ function _pollForEvents(client) { }); Object.keys(roomIdsWithNewInvites).forEach(function(inviteRoomId) { - _resolveInvites(self.store.getRoom(inviteRoomId)); + _resolveInvites(self, self.store.getRoom(inviteRoomId)); }); } if (data) { @@ -2198,7 +2198,7 @@ function reEmit(reEmitEntity, emittableEntity, eventNames) { } function _resolveInvites(client, room) { - if (!room || !client.resolveInvitesToProfiles) { + if (!room || !client._config.resolveInvitesToProfiles) { return; } // For each invited room member we want to give them a displayname/avatar url diff --git a/spec/integ/matrix-client-syncing.spec.js b/spec/integ/matrix-client-syncing.spec.js index 6e85f0c36..b49e09ef9 100644 --- a/spec/integ/matrix-client-syncing.spec.js +++ b/spec/integ/matrix-client-syncing.spec.js @@ -10,6 +10,11 @@ describe("MatrixClient syncing", function() { var selfUserId = "@alice:localhost"; var selfAccessToken = "aseukfgwef"; var otherUserId = "@bob:localhost"; + var userA = "@alice:bar"; + var userB = "@bob:bar"; + var userC = "@claire:bar"; + var roomOne = "!foo:localhost"; + var roomTwo = "!bar:localhost"; beforeEach(function() { utils.beforeEach(this); @@ -65,10 +70,124 @@ describe("MatrixClient syncing", function() { }); }); + describe("resolving invites to profile info", function() { + var initialSync = { + end: "s_5_3", + presence: [], + rooms: [{ + membership: "join", + room_id: roomOne, + messages: { + start: "f_1_1", + end: "f_2_2", + chunk: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello" + }) + ] + }, + state: [ + utils.mkMembership({ + room: roomOne, mship: "join", user: otherUserId + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: selfUserId + }), + utils.mkEvent({ + type: "m.room.create", room: roomOne, user: selfUserId, + content: { + creator: selfUserId + } + }) + ] + }] + }; + var eventData = { + start: "s_5_3", + end: "e_6_7", + chunk: [] + }; + + beforeEach(function() { + eventData.chunk = []; + }); + + it("should resolve incoming invites from /events", function(done) { + eventData.chunk = [ + utils.mkMembership({ + room: roomOne, mship: "invite", user: userC + }) + ]; + + httpBackend.when("GET", "/initialSync").respond(200, initialSync); + httpBackend.when("GET", "/events").respond(200, eventData); + httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond(200, { + avatar_url: "mxc://flibble/wibble", + displayname: "The Boss" + }); + + client.startClient({ + resolveInvitesToProfiles: true + }); + + httpBackend.flush().done(function() { + var member = client.getRoom(roomOne).getMember(userC); + expect(member.name).toEqual("The Boss"); + expect( + member.getAvatarUrl("home.server.url", null, null, null, false) + ).toBeDefined(); + done(); + }); + }); + + it("should use cached values from m.presence wherever possible", function(done) { + eventData.chunk = [ + utils.mkPresence({ + user: userC, presence: "online", name: "The Ghost" + }), + utils.mkMembership({ + room: roomOne, mship: "invite", user: userC + }) + ]; + + httpBackend.when("GET", "/initialSync").respond(200, initialSync); + httpBackend.when("GET", "/events").respond(200, eventData); + + client.startClient({ + resolveInvitesToProfiles: true + }); + + httpBackend.flush().done(function() { + var member = client.getRoom(roomOne).getMember(userC); + expect(member.name).toEqual("The Ghost"); + done(); + }); + }); + + it("should no-op if resolveInvitesToProfiles is not set", function(done) { + eventData.chunk = [ + utils.mkMembership({ + room: roomOne, mship: "invite", user: userC + }) + ]; + + httpBackend.when("GET", "/initialSync").respond(200, initialSync); + httpBackend.when("GET", "/events").respond(200, eventData); + + client.startClient(); + + httpBackend.flush().done(function() { + var member = client.getRoom(roomOne).getMember(userC); + expect(member.name).toEqual(userC); + expect( + member.getAvatarUrl("home.server.url", null, null, null, false) + ).toBeNull(); + done(); + }); + }); + }); + describe("users", function() { - var userA = "@alice:bar"; - var userB = "@bob:bar"; - var userC = "@claire:bar"; var initialSync = { end: "s_5_3", presence: [ @@ -113,8 +232,6 @@ describe("MatrixClient syncing", function() { }); describe("room state", function() { - var roomOne = "!foo:localhost"; - var roomTwo = "!bar:localhost"; var msgText = "some text here"; var otherDisplayName = "Bob Smith"; var initialSync = { @@ -272,7 +389,6 @@ describe("MatrixClient syncing", function() { }); describe("receipts", function() { - var roomOne = "!foo:localhost"; var initialSync = { end: "s_5_3", presence: [], diff --git a/spec/mock-request.js b/spec/mock-request.js index 4e10f6275..9a541b757 100644 --- a/spec/mock-request.js +++ b/spec/mock-request.js @@ -13,6 +13,7 @@ function HttpBackend() { this.requestFn = function(opts, callback) { var realReq = new Request(opts.method, opts.uri, opts.body, opts.qs); realReq.callback = callback; + console.log("HTTP backend received request: %s %s", opts.method, opts.uri); self.requests.push(realReq); }; } From aa3e6514c62537ecce9247a98cf6c72abca3f0f5 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 26 Oct 2015 16:30:15 +0000 Subject: [PATCH 3/4] Add test for firing (pew pew) of events --- spec/integ/matrix-client-syncing.spec.js | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spec/integ/matrix-client-syncing.spec.js b/spec/integ/matrix-client-syncing.spec.js index b49e09ef9..66fae69e4 100644 --- a/spec/integ/matrix-client-syncing.spec.js +++ b/spec/integ/matrix-client-syncing.spec.js @@ -164,6 +164,36 @@ describe("MatrixClient syncing", function() { }); }); + it("should result in events on the room member firing", function(done) { + eventData.chunk = [ + utils.mkPresence({ + user: userC, presence: "online", name: "The Ghost" + }), + utils.mkMembership({ + room: roomOne, mship: "invite", user: userC + }) + ]; + + httpBackend.when("GET", "/initialSync").respond(200, initialSync); + httpBackend.when("GET", "/events").respond(200, eventData); + + var latestFiredName = null; + client.on("RoomMember.name", function(event, m) { + if (m.userId === userC && m.roomId === roomOne) { + latestFiredName = m.name; + } + }); + + client.startClient({ + resolveInvitesToProfiles: true + }); + + httpBackend.flush().done(function() { + expect(latestFiredName).toEqual("The Ghost"); + done(); + }); + }); + it("should no-op if resolveInvitesToProfiles is not set", function(done) { eventData.chunk = [ utils.mkMembership({ From 2675442ced9ece24ee9ac17e874c52971429167a Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 26 Oct 2015 16:31:10 +0000 Subject: [PATCH 4/4] Line lengths --- spec/integ/matrix-client-syncing.spec.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/integ/matrix-client-syncing.spec.js b/spec/integ/matrix-client-syncing.spec.js index 66fae69e4..7238f6429 100644 --- a/spec/integ/matrix-client-syncing.spec.js +++ b/spec/integ/matrix-client-syncing.spec.js @@ -121,10 +121,12 @@ describe("MatrixClient syncing", function() { httpBackend.when("GET", "/initialSync").respond(200, initialSync); httpBackend.when("GET", "/events").respond(200, eventData); - httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond(200, { - avatar_url: "mxc://flibble/wibble", - displayname: "The Boss" - }); + httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond( + 200, { + avatar_url: "mxc://flibble/wibble", + displayname: "The Boss" + } + ); client.startClient({ resolveInvitesToProfiles: true