diff --git a/lib/client.js b/lib/client.js index bef4b8bc8..afd652f47 100644 --- a/lib/client.js +++ b/lib/client.js @@ -169,7 +169,7 @@ function MatrixClient(opts) { } this._syncState = null; this._syncingRetry = null; - this._guestRooms = null; + this._peekSync = null; this._isGuest = false; this._ongoingScrollbacks = {}; } @@ -191,6 +191,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. @@ -621,6 +640,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); @@ -1284,6 +1304,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, @@ -1347,6 +1371,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 @@ -2022,11 +2050,46 @@ 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. + * @return {module:client.Promise} Resolves: Room object + * @return {module:http-api.MatrixError} Rejects: with an error response. */ -MatrixClient.prototype.setGuestRooms = function(roomIds) { - this._guestRooms = roomIds; +MatrixClient.prototype.peekInRoom = function(roomId) { + if (this._peekSync) { + this._peekSync.stopPeeking(); + } + this._peekSync = new SyncApi(this); + return this._peekSync.peek(roomId); +}; + +/** + * 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); }; /** @@ -2035,12 +2098,14 @@ MatrixClient.prototype.setGuestRooms = function(roomIds) { * @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; } @@ -2050,6 +2115,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, @@ -2701,6 +2767,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 " + diff --git a/lib/sync.js b/lib/sync.js index cb43b347f..545197b2d 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -59,6 +59,7 @@ function SyncApi(client, opts) { opts.pollTimeout = opts.pollTimeout || (30 * 1000); opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; this.opts = opts; + this._peekRoomId = null; } /** @@ -154,6 +155,100 @@ 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; + }); +}; + +/** + * Stop polling for updates in the peeked room. NOPs if there is no room being + * 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); + }); +}; + /** * Main entry point */ @@ -205,8 +300,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(); @@ -225,8 +320,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' }; @@ -238,10 +338,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 @@ -562,7 +658,25 @@ 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); +}; +/** + * @return {string} + */ +SyncApi.prototype._getGuestFilter = function() { + var guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching + 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: { + limit: 20 + } + } + }); }; function retryTimeMsForAttempt(attempt) { diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js index faf88cd05..badbabf74 100644 --- a/spec/unit/matrix-client.spec.js +++ b/spec/unit/matrix-client.spec.js @@ -418,27 +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) { - httpLookups = []; // no /pushrules - httpLookups.push(FILTER_RESPONSE); + 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: { - room_id: 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) { + }); }); });