From 8c6c65ab6c48ec4ceda4b55a71ccf3cb532902e4 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 5 Jan 2016 11:50:24 +0000 Subject: [PATCH 01/10] Don't do requests we know are going to fail as a guest --- lib/client.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/client.js b/lib/client.js index e66d25b51..3172a9f2b 100644 --- a/lib/client.js +++ b/lib/client.js @@ -2502,6 +2502,10 @@ function checkTurnServers(client) { if (!client._supportsVoip) { return; } + if (client.isGuest()) { + return; // guests can't access TURN servers + } + client.turnServer().done(function(res) { if (res.uris) { console.log("Got TURN URIs: " + res.uris + " refresh in " + From f12499c6bfe47d9527bdcbd8b4bc212ea46aedf9 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 5 Jan 2016 13:24:51 +0000 Subject: [PATCH 02/10] Support guest room filters --- lib/sync.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/sync.js b/lib/sync.js index ff4e8ba9d..10b68ffc4 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -190,8 +190,8 @@ SyncApi.prototype.sync = function() { } if (client.isGuest()) { - // no push rules for guests - getFilter(); + // no push rules for guests, no access to POST filter for guests. + self._sync({}); } else { getPushRules(); @@ -210,8 +210,13 @@ SyncApi.prototype._sync = function(syncOptions, attempt) { var self = this; attempt = attempt || 1; + var filterId = syncOptions.filterId; + if (client.isGuest() && !filterId) { + filterId = this._getGuestFilter(); + } + var qps = { - filter: syncOptions.filterId, + filter: filterId, timeout: this.opts.pollTimeout, since: client.store.getSyncToken() || undefined // do not send 'null' }; @@ -223,10 +228,6 @@ SyncApi.prototype._sync = function(syncOptions, attempt) { qps.timeout = 1; } - if (client._guestRooms && client._isGuest) { - qps.room_id = client._guestRooms; - } - client._http.authedRequestWithPrefix( undefined, "GET", "/sync", qps, undefined, httpApi.PREFIX_V2_ALPHA, this.opts.pollTimeout + BUFFER_PERIOD_MS // normal timeout= plus buffer time @@ -543,7 +544,18 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList, // execute the timeline events, this will begin to diverge the current state // if the timeline has any state events in it. room.addEventsToTimeline(timelineEventList); +}; +SyncApi.prototype._getGuestFilter = function() { + var guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching + if (!guestRooms) { + return "{}"; + } + return JSON.stringify({ + room: { + rooms: guestRooms + } + }); }; function retryTimeMsForAttempt(attempt) { From 445491c4ad1c59a39a0f20876fecb087158799d2 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 5 Jan 2016 16:57:59 +0000 Subject: [PATCH 03/10] Fix guest rooms UT to reflect reality --- spec/unit/matrix-client.spec.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js index faf88cd05..f50b0e68f 100644 --- a/spec/unit/matrix-client.spec.js +++ b/spec/unit/matrix-client.spec.js @@ -423,14 +423,13 @@ describe("MatrixClient", function() { ]; it("should be set via setGuestRooms and used in /sync calls", function(done) { - httpLookups = []; // no /pushrules - httpLookups.push(FILTER_RESPONSE); + httpLookups = []; // no /pushrules or /filter httpLookups.push({ method: "GET", path: "/sync", data: SYNC_DATA, expectQueryParams: { - room_id: roomIds + filter: JSON.stringify({ room: { rooms: roomIds } }) }, thenCall: function() { done(); From 73e65bc18bcd1179a7131f328201297320d238d8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 5 Jan 2016 17:09:10 +0000 Subject: [PATCH 04/10] Add setGuestAccess to allow easy room guest access configuration. --- lib/client.js | 28 ++++++++++++++++++++++++++++ lib/sync.js | 3 +++ 2 files changed, 31 insertions(+) diff --git a/lib/client.js b/lib/client.js index 3172a9f2b..2440ac44e 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1946,6 +1946,34 @@ MatrixClient.prototype.setGuestRooms = function(roomIds) { this._guestRooms = roomIds; }; +/** + * Set r/w flags for guest access in a room. + * @param {string} roomId The room to configure guest access in. + * @param {Object} opts Options + * @param {boolean} opts.allowJoin True to allow guests to join this room. This + * implicitly gives guests write access. If false or not given, guests are + * explicitly forbidden from joining the room. + * @param {boolean} opts.allowRead True to set history visibility to + * be world_readable. This gives guests read access *from this point forward*. + * If false or not given, history visibility is not modified. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setGuestAccess = function(roomId, opts) { + var writePromise = this.sendStateEvent(roomId, "m.room.guest_access", { + guest_access: opts.allowJoin ? "can_join" : "forbidden" + }); + + var readPromise = q(); + if (opts.allowRead) { + readPromise = this.sendStateEvent(roomId, "m.room.history_visibility", { + history_visibility: "world_readable" + }); + } + + return q.all(readPromise, writePromise); +}; + /** * @param {string} username * @param {string} password diff --git a/lib/sync.js b/lib/sync.js index 10b68ffc4..58b4c3239 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -546,6 +546,9 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList, room.addEventsToTimeline(timelineEventList); }; +/** + * @return {string} + */ SyncApi.prototype._getGuestFilter = function() { var guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching if (!guestRooms) { From 3a3f25c1bc15be9143f1ca9bf337d1c4b92b9766 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 6 Jan 2016 17:29:14 +0000 Subject: [PATCH 05/10] Remove guest rooms array; replace with a peeking SyncApi After much discussion, the HS will now behave the same for guests/non-guests wrt joining a room (you get the entire room state on join). This leave "peeking" which never triggers a join. This can be implemented for guests by doing a room initial sync followed by a specific /events poll with a specific room_id. This means there are 2 sync streams: /sync and the peek /events. Architected so you can only have 1 peek stream in progress at a time (if this were arbitrary we'd quickly run into concurrent in-flight browser request limits (5). --- lib/client.js | 16 +++++++++++----- lib/sync.js | 21 ++++++++++++++++++++- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/lib/client.js b/lib/client.js index 2440ac44e..0ce1576f5 100644 --- a/lib/client.js +++ b/lib/client.js @@ -153,7 +153,7 @@ function MatrixClient(opts) { } this._syncState = null; this._syncingRetry = null; - this._guestRooms = null; + this._peekSync = null; this._isGuest = false; this._ongoingScrollbacks = {}; } @@ -605,6 +605,7 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) { opts = opts || { syncRoom: true }; + var room = this.getRoom(roomIdOrAlias); if (room && room.hasMembershipState(this.credentials.userId, "join")) { return q(room); @@ -1939,11 +1940,16 @@ MatrixClient.prototype.registerGuest = function(opts, callback) { }; /** - * Set a list of rooms the Guest is interested in receiving events from. - * @param {String[]} roomIds A list of room IDs. + * Peek into a room and receive updates about the room. This only works if the + * history visibility for the room is world_readable. + * @param {String} roomId The room to attempt to peek into. */ -MatrixClient.prototype.setGuestRooms = function(roomIds) { - this._guestRooms = roomIds; +MatrixClient.prototype.peekInRoom = function(roomId) { + if (this._peekSync) { + this._peekSync.stopPeeking(); + } + this._peekSync = new SyncApi(this); + this._peekSync.peek(roomId); }; /** diff --git a/lib/sync.js b/lib/sync.js index 58b4c3239..1bc7335ac 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -139,6 +139,23 @@ SyncApi.prototype.syncLeftRooms = function() { }); }; +/** + * Peek into a room. This will result in the room in question being synced so it + * is accessible via getRooms(). Live updates for the room will be provided. + * @param {string} roomId The room ID to peek into. + */ +SyncApi.prototype.peek = function(roomId) { + +}; + +/** + * Stop polling for updates in the peeked room. NOPs if there is no room being + * peeked. + */ +SyncApi.prototype.stopPeeking = function() { + +}; + /** * Main entry point */ @@ -556,7 +573,9 @@ SyncApi.prototype._getGuestFilter = function() { } return JSON.stringify({ room: { - rooms: guestRooms + timeline: { + limit: 20 + } } }); }; From d36c928d95ac143443d2c5cacdc78644b41962a6 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 6 Jan 2016 17:35:56 +0000 Subject: [PATCH 06/10] Fix tests --- spec/unit/matrix-client.spec.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js index f50b0e68f..badbabf74 100644 --- a/spec/unit/matrix-client.spec.js +++ b/spec/unit/matrix-client.spec.js @@ -418,26 +418,22 @@ describe("MatrixClient", function() { }); describe("guest rooms", function() { - var roomIds = [ - "!foo:bar", "!baz:bar" - ]; - it("should be set via setGuestRooms and used in /sync calls", function(done) { + it("should only do /sync calls (without filter/pushrules)", function(done) { httpLookups = []; // no /pushrules or /filter httpLookups.push({ method: "GET", path: "/sync", data: SYNC_DATA, - expectQueryParams: { - filter: JSON.stringify({ room: { rooms: roomIds } }) - }, thenCall: function() { done(); } }); - client.setGuestRooms(roomIds); client.setGuest(true); client.startClient(); }); + + xit("should be able to peek into a room using peekInRoom", function(done) { + }); }); }); From cdb4bc51074b7b03d4501e1d8dacff3e6ebfa197 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 7 Jan 2016 14:58:28 +0000 Subject: [PATCH 07/10] Implement peek syncing. This involves hitting room initial sync then /events?room_id=!thing:here It even works. --- lib/client.js | 4 ++- lib/sync.js | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/lib/client.js b/lib/client.js index 0ce1576f5..106dfea26 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1943,13 +1943,15 @@ MatrixClient.prototype.registerGuest = function(opts, callback) { * Peek into a room and receive updates about the room. This only works if the * history visibility for the room is world_readable. * @param {String} roomId The room to attempt to peek into. + * @return {module:client.Promise} Resolves: Room object + * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.peekInRoom = function(roomId) { if (this._peekSync) { this._peekSync.stopPeeking(); } this._peekSync = new SyncApi(this); - this._peekSync.peek(roomId); + return this._peekSync.peek(roomId); }; /** diff --git a/lib/sync.js b/lib/sync.js index 1bc7335ac..e7299a39b 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -44,6 +44,7 @@ function SyncApi(client, opts) { opts.pollTimeout = opts.pollTimeout || (30 * 1000); opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; this.opts = opts; + this._peekRoomId = null; } /** @@ -143,9 +144,53 @@ SyncApi.prototype.syncLeftRooms = function() { * Peek into a room. This will result in the room in question being synced so it * is accessible via getRooms(). Live updates for the room will be provided. * @param {string} roomId The room ID to peek into. + * @return {Promise} A promise which resolves once the room has been added to the + * store. */ SyncApi.prototype.peek = function(roomId) { + var self = this; + var client = this.client; + this._peekRoomId = roomId; + return this.client.roomInitialSync(roomId, 20).then(function(response) { + // make sure things are init'd + response.messages = response.messages || {}; + response.messages.chunk = response.messages.chunk || []; + response.state = response.state || []; + var peekRoom = self.createRoom(roomId); + + // FIXME: Mostly duplicated from _processRoomEvents but not entirely + // because "state" in this API is at the BEGINNING of the chunk + var oldStateEvents = utils.map( + utils.deepCopy(response.state), client.getEventMapper() + ); + var stateEvents = utils.map( + response.state, client.getEventMapper() + ); + var messages = utils.map( + response.messages.chunk, client.getEventMapper() + ); + + if (response.messages.start) { + peekRoom.oldState.paginationToken = response.messages.start; + } + + // set the state of the room to as it was after the timeline executes + peekRoom.oldState.setStateEvents(oldStateEvents); + peekRoom.currentState.setStateEvents(stateEvents); + + self._resolveInvites(peekRoom); + peekRoom.recalculate(self.client.credentials.userId); + + // roll backwards to diverge old state: + peekRoom.addEventsToTimeline(messages.reverse(), true); + + client.store.storeRoom(peekRoom); + client.emit("Room", peekRoom); + + self._peekPoll(roomId); + return peekRoom; + }); }; /** @@ -153,7 +198,40 @@ SyncApi.prototype.peek = function(roomId) { * peeked. */ SyncApi.prototype.stopPeeking = function() { + this._peekRoomId = null; +}; +/** + * Do a peek room poll. + * @param {string} roomId + * @param {string} token from= token + */ +SyncApi.prototype._peekPoll = function(roomId, token) { + if (this._peekRoomId !== roomId) { + console.log("Stopped peeking in room %s", roomId); + return; + } + + var self = this; + // FIXME: gut wrenching; hard-coded timeout values + this.client._http.authedRequestWithPrefix(undefined, "GET", "/events", { + room_id: roomId, + timeout: 30 * 1000, + from: token + }, undefined, httpApi.PREFIX_V1, 50 * 1000).done(function(res) { + // strip out events which aren't for the given room_id (e.g presence) + var events = res.chunk.filter(function(e) { + return e.room_id === roomId; + }).map(self.client.getEventMapper()); + var room = self.client.getRoom(roomId); + room.addEvents(events); + self._peekPoll(roomId, res.end); + }, function(err) { + console.error("[%s] Peek poll failed: %s", roomId, err); + setTimeout(function() { + self._peekPoll(roomId, token); + }, 30 * 1000); + }); }; /** From 8f4bd9c693fa7aeb063a1f4d35998115d5dcbe41 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 7 Jan 2016 15:03:37 +0000 Subject: [PATCH 08/10] NOP typing/receipts when a guest --- lib/client.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/client.js b/lib/client.js index 106dfea26..540eff06d 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1269,6 +1269,10 @@ MatrixClient.prototype.sendHtmlEmote = function(roomId, body, htmlBody, callback * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.sendReceipt = function(event, receiptType, callback) { + if (this.isGuest()) { + return q({}); // guests cannot send receipts so don't bother. + } + var path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", { $roomId: event.getRoomId(), $receiptType: receiptType, @@ -1332,6 +1336,10 @@ MatrixClient.prototype.getCurrentUploads = function() { * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.sendTyping = function(roomId, isTyping, timeoutMs, callback) { + if (this.isGuest()) { + return q({}); // guests cannot send typing notifications so don't bother. + } + var path = utils.encodeUri("/rooms/$roomId/typing/$userId", { $roomId: roomId, $userId: this.credentials.userId From ea3bd1450e7008f0fcd44c257e052809d4ef7247 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 7 Jan 2016 17:23:56 +0000 Subject: [PATCH 09/10] Add functions to support upgrading guest accounts --- lib/client.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/client.js b/lib/client.js index 540eff06d..0acb16fe2 100644 --- a/lib/client.js +++ b/lib/client.js @@ -175,6 +175,25 @@ MatrixClient.prototype.getIdentityServerUrl = function() { return this.idBaseUrl; }; +/** + * Get the access token associated with this account. + * @return {?String} The access_token or null + */ +MatrixClient.prototype.getAccessToken = function() { + return this._http.opts.accessToken || null; +}; + +/** + * Get the local part of the current user ID e.g. "foo" in "@foo:bar". + * @return {?String} The user ID localpart or null. + */ +MatrixClient.prototype.getUserIdLocalpart = function() { + if (this.credentials && this.credentials.userId) { + return this.credentials.userId.split(":")[0].substring(1); + } + return null; +}; + /** * Check if the runtime environment supports VoIP calling. * @return {boolean} True if VoIP is supported. @@ -1996,12 +2015,14 @@ MatrixClient.prototype.setGuestAccess = function(roomId, opts) { * @param {string} sessionId * @param {Object} auth * @param {boolean} bindEmail + * @param {string} guestAccessToken * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: TODO * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.register = function(username, password, - sessionId, auth, bindEmail, callback) { + sessionId, auth, bindEmail, guestAccessToken, + callback) { if (auth === undefined) { auth = {}; } if (sessionId) { auth.session = sessionId; } @@ -2011,6 +2032,7 @@ MatrixClient.prototype.register = function(username, password, if (username !== undefined) { params.username = username; } if (password !== undefined) { params.password = password; } if (bindEmail !== undefined) { params.bind_email = bindEmail; } + if (guestAccessToken !== undefined) { params.guest_access_token = guestAccessToken; } return this._http.requestWithPrefix( callback, "POST", "/register", undefined, From 4fe95f18b9edcbb646100dcb479a08f56e4aced9 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 11 Jan 2016 14:52:06 +0000 Subject: [PATCH 10/10] More commenting on the creation of guest filters --- lib/sync.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/sync.js b/lib/sync.js index e7299a39b..cb381f0d7 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -649,6 +649,8 @@ SyncApi.prototype._getGuestFilter = function() { if (!guestRooms) { return "{}"; } + // we just need to specify the filter inline if we're a guest because guests + // can't create filters. return JSON.stringify({ room: { timeline: {