UNPKG

74.2 kBJavaScriptView Raw
1/*
2Copyright 2015, 2016 OpenMarket Ltd
3Copyright 2017 Vector Creations Ltd
4Copyright 2018 New Vector Ltd
5
6Licensed under the Apache License, Version 2.0 (the "License");
7you may not use this file except in compliance with the License.
8You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12Unless required by applicable law or agreed to in writing, software
13distributed under the License is distributed on an "AS IS" BASIS,
14WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15See the License for the specific language governing permissions and
16limitations under the License.
17*/
18"use strict";
19
20/*
21 * TODO:
22 * This class mainly serves to take all the syncing logic out of client.js and
23 * into a separate file. It's all very fluid, and this class gut wrenches a lot
24 * of MatrixClient props (e.g. _http). Given we want to support WebSockets as
25 * an alternative syncing API, we may want to have a proper syncing interface
26 * for HTTP and WS at some point.
27 */
28
29var _stringify = require("babel-runtime/core-js/json/stringify");
30
31var _stringify2 = _interopRequireDefault(_stringify);
32
33var _keys = require("babel-runtime/core-js/object/keys");
34
35var _keys2 = _interopRequireDefault(_keys);
36
37var _getIterator2 = require("babel-runtime/core-js/get-iterator");
38
39var _getIterator3 = _interopRequireDefault(_getIterator2);
40
41var _regenerator = require("babel-runtime/regenerator");
42
43var _regenerator2 = _interopRequireDefault(_regenerator);
44
45var _bluebird = require("bluebird");
46
47var _bluebird2 = _interopRequireDefault(_bluebird);
48
49function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
50
51var User = require("./models/user");
52var Room = require("./models/room");
53var Group = require('./models/group');
54var utils = require("./utils");
55var Filter = require("./filter");
56var EventTimeline = require("./models/event-timeline");
57
58var DEBUG = true;
59
60// /sync requests allow you to set a timeout= but the request may continue
61// beyond that and wedge forever, so we need to track how long we are willing
62// to keep open the connection. This constant is *ADDED* to the timeout= value
63// to determine the max time we're willing to wait.
64var BUFFER_PERIOD_MS = 80 * 1000;
65
66// Number of consecutive failed syncs that will lead to a syncState of ERROR as opposed
67// to RECONNECTING. This is needed to inform the client of server issues when the
68// keepAlive is successful but the server /sync fails.
69var FAILED_SYNC_ERROR_THRESHOLD = 3;
70
71function getFilterName(userId, suffix) {
72 // scope this on the user ID because people may login on many accounts
73 // and they all need to be stored!
74 return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
75}
76
77function debuglog() {
78 var _console;
79
80 if (!DEBUG) {
81 return;
82 }
83 (_console = console).log.apply(_console, arguments);
84}
85
86/**
87 * <b>Internal class - unstable.</b>
88 * Construct an entity which is able to sync with a homeserver.
89 * @constructor
90 * @param {MatrixClient} client The matrix client instance to use.
91 * @param {Object} opts Config options
92 * @param {module:crypto=} opts.crypto Crypto manager
93 * @param {Function=} opts.canResetEntireTimeline A function which is called
94 * with a room ID and returns a boolean. It should return 'true' if the SDK can
95 * SAFELY remove events from this room. It may not be safe to remove events if
96 * there are other references to the timelines for this room.
97 * Default: returns false.
98 * @param {Boolean=} opts.disablePresence True to perform syncing without automatically
99 * updating presence.
100 */
101function SyncApi(client, opts) {
102 this.client = client;
103 opts = opts || {};
104 opts.initialSyncLimit = opts.initialSyncLimit === undefined ? 8 : opts.initialSyncLimit;
105 opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false;
106 opts.pollTimeout = opts.pollTimeout || 30 * 1000;
107 opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
108 if (!opts.canResetEntireTimeline) {
109 opts.canResetEntireTimeline = function (roomId) {
110 return false;
111 };
112 }
113 this.opts = opts;
114 this._peekRoomId = null;
115 this._currentSyncRequest = null;
116 this._syncState = null;
117 this._catchingUp = false;
118 this._running = false;
119 this._keepAliveTimer = null;
120 this._connectionReturnedDefer = null;
121 this._notifEvents = []; // accumulator of sync events in the current sync response
122 this._failedSyncCount = 0; // Number of consecutive failed /sync requests
123
124 if (client.getNotifTimelineSet()) {
125 client.reEmitter.reEmit(client.getNotifTimelineSet(), ["Room.timeline", "Room.timelineReset"]);
126 }
127}
128
129/**
130 * @param {string} roomId
131 * @return {Room}
132 */
133SyncApi.prototype.createRoom = function (roomId) {
134 var client = this.client;
135 var room = new Room(roomId, {
136 pendingEventOrdering: this.opts.pendingEventOrdering,
137 timelineSupport: client.timelineSupport
138 });
139 client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction", "Room.receipt", "Room.tags", "Room.timelineReset", "Room.localEchoUpdated", "Room.accountData"]);
140 this._registerStateListeners(room);
141 return room;
142};
143
144/**
145 * @param {string} groupId
146 * @return {Group}
147 */
148SyncApi.prototype.createGroup = function (groupId) {
149 var client = this.client;
150 var group = new Group(groupId);
151 client.reEmitter.reEmit(group, ["Group.profile", "Group.myMembership"]);
152 client.store.storeGroup(group);
153 return group;
154};
155
156/**
157 * @param {Room} room
158 * @private
159 */
160SyncApi.prototype._registerStateListeners = function (room) {
161 var client = this.client;
162 // we need to also re-emit room state and room member events, so hook it up
163 // to the client now. We need to add a listener for RoomState.members in
164 // order to hook them correctly. (TODO: find a better way?)
165 client.reEmitter.reEmit(room.currentState, ["RoomState.events", "RoomState.members", "RoomState.newMember"]);
166 room.currentState.on("RoomState.newMember", function (event, state, member) {
167 member.user = client.getUser(member.userId);
168 client.reEmitter.reEmit(member, ["RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel", "RoomMember.membership"]);
169 });
170};
171
172/**
173 * @param {Room} room
174 * @private
175 */
176SyncApi.prototype._deregisterStateListeners = function (room) {
177 // could do with a better way of achieving this.
178 room.currentState.removeAllListeners("RoomState.events");
179 room.currentState.removeAllListeners("RoomState.members");
180 room.currentState.removeAllListeners("RoomState.newMember");
181};
182
183/**
184 * Sync rooms the user has left.
185 * @return {Promise} Resolved when they've been added to the store.
186 */
187SyncApi.prototype.syncLeftRooms = function () {
188 var client = this.client;
189 var self = this;
190
191 // grab a filter with limit=1 and include_leave=true
192 var filter = new Filter(this.client.credentials.userId);
193 filter.setTimelineLimit(1);
194 filter.setIncludeLeaveRooms(true);
195
196 var localTimeoutMs = this.opts.pollTimeout + BUFFER_PERIOD_MS;
197 var qps = {
198 timeout: 0 };
199
200 return client.getOrCreateFilter(getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter).then(function (filterId) {
201 qps.filter = filterId;
202 return client._http.authedRequest(undefined, "GET", "/sync", qps, undefined, localTimeoutMs);
203 }).then(function (data) {
204 var leaveRooms = [];
205 if (data.rooms && data.rooms.leave) {
206 leaveRooms = self._mapSyncResponseToRoomArray(data.rooms.leave);
207 }
208 var rooms = [];
209 leaveRooms.forEach(function (leaveObj) {
210 var room = leaveObj.room;
211 rooms.push(room);
212 if (!leaveObj.isBrandNewRoom) {
213 // the intention behind syncLeftRooms is to add in rooms which were
214 // *omitted* from the initial /sync. Rooms the user were joined to
215 // but then left whilst the app is running will appear in this list
216 // and we do not want to bother with them since they will have the
217 // current state already (and may get dupe messages if we add
218 // yet more timeline events!), so skip them.
219 // NB: When we persist rooms to localStorage this will be more
220 // complicated...
221 return;
222 }
223 leaveObj.timeline = leaveObj.timeline || {};
224 var timelineEvents = self._mapSyncEventsFormat(leaveObj.timeline, room);
225 var stateEvents = self._mapSyncEventsFormat(leaveObj.state, room);
226
227 // set the back-pagination token. Do this *before* adding any
228 // events so that clients can start back-paginating.
229 room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS);
230
231 self._processRoomEvents(room, stateEvents, timelineEvents);
232
233 room.recalculate(client.credentials.userId);
234 client.store.storeRoom(room);
235 client.emit("Room", room);
236
237 self._processEventsForNotifs(room, timelineEvents);
238 });
239 return rooms;
240 });
241};
242
243/**
244 * Peek into a room. This will result in the room in question being synced so it
245 * is accessible via getRooms(). Live updates for the room will be provided.
246 * @param {string} roomId The room ID to peek into.
247 * @return {Promise} A promise which resolves once the room has been added to the
248 * store.
249 */
250SyncApi.prototype.peek = function (roomId) {
251 var self = this;
252 var client = this.client;
253 this._peekRoomId = roomId;
254 return this.client.roomInitialSync(roomId, 20).then(function (response) {
255 // make sure things are init'd
256 response.messages = response.messages || {};
257 response.messages.chunk = response.messages.chunk || [];
258 response.state = response.state || [];
259
260 var peekRoom = self.createRoom(roomId);
261
262 // FIXME: Mostly duplicated from _processRoomEvents but not entirely
263 // because "state" in this API is at the BEGINNING of the chunk
264 var oldStateEvents = utils.map(utils.deepCopy(response.state), client.getEventMapper());
265 var stateEvents = utils.map(response.state, client.getEventMapper());
266 var messages = utils.map(response.messages.chunk, client.getEventMapper());
267
268 // XXX: copypasted from /sync until we kill off this
269 // minging v1 API stuff)
270 // handle presence events (User objects)
271 if (response.presence && utils.isArray(response.presence)) {
272 response.presence.map(client.getEventMapper()).forEach(function (presenceEvent) {
273 var user = client.store.getUser(presenceEvent.getContent().user_id);
274 if (user) {
275 user.setPresenceEvent(presenceEvent);
276 } else {
277 user = createNewUser(client, presenceEvent.getContent().user_id);
278 user.setPresenceEvent(presenceEvent);
279 client.store.storeUser(user);
280 }
281 client.emit("event", presenceEvent);
282 });
283 }
284
285 // set the pagination token before adding the events in case people
286 // fire off pagination requests in response to the Room.timeline
287 // events.
288 if (response.messages.start) {
289 peekRoom.oldState.paginationToken = response.messages.start;
290 }
291
292 // set the state of the room to as it was after the timeline executes
293 peekRoom.oldState.setStateEvents(oldStateEvents);
294 peekRoom.currentState.setStateEvents(stateEvents);
295
296 self._resolveInvites(peekRoom);
297 peekRoom.recalculate(self.client.credentials.userId);
298
299 // roll backwards to diverge old state. addEventsToTimeline
300 // will overwrite the pagination token, so make sure it overwrites
301 // it with the right thing.
302 peekRoom.addEventsToTimeline(messages.reverse(), true, peekRoom.getLiveTimeline(), response.messages.start);
303
304 client.store.storeRoom(peekRoom);
305 client.emit("Room", peekRoom);
306
307 self._peekPoll(peekRoom);
308 return peekRoom;
309 });
310};
311
312/**
313 * Stop polling for updates in the peeked room. NOPs if there is no room being
314 * peeked.
315 */
316SyncApi.prototype.stopPeeking = function () {
317 this._peekRoomId = null;
318};
319
320/**
321 * Do a peek room poll.
322 * @param {Room} peekRoom
323 * @param {string} token from= token
324 */
325SyncApi.prototype._peekPoll = function (peekRoom, token) {
326 if (this._peekRoomId !== peekRoom.roomId) {
327 debuglog("Stopped peeking in room %s", peekRoom.roomId);
328 return;
329 }
330
331 var self = this;
332 // FIXME: gut wrenching; hard-coded timeout values
333 this.client._http.authedRequest(undefined, "GET", "/events", {
334 room_id: peekRoom.roomId,
335 timeout: 30 * 1000,
336 from: token
337 }, undefined, 50 * 1000).done(function (res) {
338 if (self._peekRoomId !== peekRoom.roomId) {
339 debuglog("Stopped peeking in room %s", peekRoom.roomId);
340 return;
341 }
342 // We have a problem that we get presence both from /events and /sync
343 // however, /sync only returns presence for users in rooms
344 // you're actually joined to.
345 // in order to be sure to get presence for all of the users in the
346 // peeked room, we handle presence explicitly here. This may result
347 // in duplicate presence events firing for some users, which is a
348 // performance drain, but such is life.
349 // XXX: copypasted from /sync until we can kill this minging v1 stuff.
350
351 res.chunk.filter(function (e) {
352 return e.type === "m.presence";
353 }).map(self.client.getEventMapper()).forEach(function (presenceEvent) {
354 var user = self.client.store.getUser(presenceEvent.getContent().user_id);
355 if (user) {
356 user.setPresenceEvent(presenceEvent);
357 } else {
358 user = createNewUser(self.client, presenceEvent.getContent().user_id);
359 user.setPresenceEvent(presenceEvent);
360 self.client.store.storeUser(user);
361 }
362 self.client.emit("event", presenceEvent);
363 });
364
365 // strip out events which aren't for the given room_id (e.g presence)
366 var events = res.chunk.filter(function (e) {
367 return e.room_id === peekRoom.roomId;
368 }).map(self.client.getEventMapper());
369
370 peekRoom.addLiveEvents(events);
371 self._peekPoll(peekRoom, res.end);
372 }, function (err) {
373 console.error("[%s] Peek poll failed: %s", peekRoom.roomId, err);
374 setTimeout(function () {
375 self._peekPoll(peekRoom, token);
376 }, 30 * 1000);
377 });
378};
379
380/**
381 * Returns the current state of this sync object
382 * @see module:client~MatrixClient#event:"sync"
383 * @return {?String}
384 */
385SyncApi.prototype.getSyncState = function () {
386 return this._syncState;
387};
388
389SyncApi.prototype.recoverFromSyncStartupError = function () {
390 var _ref = (0, _bluebird.coroutine)(_regenerator2.default.mark(function _callee(savedSyncPromise, err) {
391 var keepaliveProm;
392 return _regenerator2.default.wrap(function _callee$(_context) {
393 while (1) {
394 switch (_context.prev = _context.next) {
395 case 0:
396 _context.next = 2;
397 return (0, _bluebird.resolve)(savedSyncPromise);
398
399 case 2:
400 keepaliveProm = this._startKeepAlives();
401
402 this._updateSyncState("ERROR", { error: err });
403 _context.next = 6;
404 return (0, _bluebird.resolve)(keepaliveProm);
405
406 case 6:
407 case "end":
408 return _context.stop();
409 }
410 }
411 }, _callee, this);
412 }));
413
414 return function (_x, _x2) {
415 return _ref.apply(this, arguments);
416 };
417}();
418
419/**
420 * Main entry point
421 */
422SyncApi.prototype.sync = function () {
423
424 // We need to do one-off checks before we can begin the /sync loop.
425 // These are:
426 // 1) We need to get push rules so we can check if events should bing as we get
427 // them from /sync.
428 // 2) We need to get/create a filter which we can use for /sync.
429
430 var getPushRules = function () {
431 var _ref2 = (0, _bluebird.coroutine)(_regenerator2.default.mark(function _callee2() {
432 var result;
433 return _regenerator2.default.wrap(function _callee2$(_context2) {
434 while (1) {
435 switch (_context2.prev = _context2.next) {
436 case 0:
437 _context2.prev = 0;
438 _context2.next = 3;
439 return (0, _bluebird.resolve)(client.getPushRules());
440
441 case 3:
442 result = _context2.sent;
443
444 debuglog("Got push rules");
445
446 client.pushRules = result;
447 _context2.next = 14;
448 break;
449
450 case 8:
451 _context2.prev = 8;
452 _context2.t0 = _context2["catch"](0);
453 _context2.next = 12;
454 return (0, _bluebird.resolve)(self.recoverFromSyncStartupError(savedSyncPromise, _context2.t0));
455
456 case 12:
457 getPushRules();
458 return _context2.abrupt("return");
459
460 case 14:
461 getFilter(); // Now get the filter and start syncing
462
463 case 15:
464 case "end":
465 return _context2.stop();
466 }
467 }
468 }, _callee2, this, [[0, 8]]);
469 }));
470
471 return function getPushRules() {
472 return _ref2.apply(this, arguments);
473 };
474 }();
475
476 var getFilter = function () {
477 var _ref3 = (0, _bluebird.coroutine)(_regenerator2.default.mark(function _callee3() {
478 var filter, filterId;
479 return _regenerator2.default.wrap(function _callee3$(_context3) {
480 while (1) {
481 switch (_context3.prev = _context3.next) {
482 case 0:
483 filter = void 0;
484
485 if (self.opts.filter) {
486 filter = self.opts.filter;
487 } else {
488 filter = new Filter(client.credentials.userId);
489 filter.setTimelineLimit(self.opts.initialSyncLimit);
490 }
491
492 filterId = void 0;
493 _context3.prev = 3;
494 _context3.next = 6;
495 return (0, _bluebird.resolve)(client.getOrCreateFilter(getFilterName(client.credentials.userId), filter));
496
497 case 6:
498 filterId = _context3.sent;
499 _context3.next = 15;
500 break;
501
502 case 9:
503 _context3.prev = 9;
504 _context3.t0 = _context3["catch"](3);
505 _context3.next = 13;
506 return (0, _bluebird.resolve)(self.recoverFromSyncStartupError(savedSyncPromise, _context3.t0));
507
508 case 13:
509 getFilter();
510 return _context3.abrupt("return");
511
512 case 15:
513 // reset the notifications timeline to prepare it to paginate from
514 // the current point in time.
515 // The right solution would be to tie /sync pagination tokens into
516 // /notifications API somehow.
517 client.resetNotifTimelineSet();
518
519 if (self._currentSyncRequest === null) {
520 // Send this first sync request here so we can then wait for the saved
521 // sync data to finish processing before we process the results of this one.
522 console.log("Sending first sync request...");
523 self._currentSyncRequest = self._doSyncRequest({ filterId: filterId }, savedSyncToken);
524 }
525
526 // Now wait for the saved sync to finish...
527 _context3.next = 19;
528 return (0, _bluebird.resolve)(savedSyncPromise);
529
530 case 19:
531 self._sync({ filterId: filterId });
532
533 case 20:
534 case "end":
535 return _context3.stop();
536 }
537 }
538 }, _callee3, this, [[3, 9]]);
539 }));
540
541 return function getFilter() {
542 return _ref3.apply(this, arguments);
543 };
544 }();
545
546 var client = this.client;
547 var self = this;
548
549 this._running = true;
550
551 if (global.document) {
552 this._onOnlineBound = this._onOnline.bind(this);
553 global.document.addEventListener("online", this._onOnlineBound, false);
554 }
555
556 var savedSyncPromise = _bluebird2.default.resolve();
557 var savedSyncToken = null;
558
559 if (client.isGuest()) {
560 // no push rules for guests, no access to POST filter for guests.
561 self._sync({});
562 } else {
563 // Pull the saved sync token out first, before the worker starts sending
564 // all the sync data which could take a while. This will let us send our
565 // first incremental sync request before we've processed our saved data.
566 savedSyncPromise = client.store.getSavedSyncToken().then(function (tok) {
567 savedSyncToken = tok;
568 return client.store.getSavedSync();
569 }).then(function (savedSync) {
570 if (savedSync) {
571 return self._syncFromCache(savedSync);
572 }
573 });
574 // Now start the first incremental sync request: this can also
575 // take a while so if we set it going now, we can wait for it
576 // to finish while we process our saved sync data.
577 getPushRules();
578 }
579};
580
581/**
582 * Stops the sync object from syncing.
583 */
584SyncApi.prototype.stop = function () {
585 debuglog("SyncApi.stop");
586 if (global.document) {
587 global.document.removeEventListener("online", this._onOnlineBound, false);
588 this._onOnlineBound = undefined;
589 }
590 this._running = false;
591 if (this._currentSyncRequest) {
592 this._currentSyncRequest.abort();
593 }
594 if (this._keepAliveTimer) {
595 clearTimeout(this._keepAliveTimer);
596 this._keepAliveTimer = null;
597 }
598};
599
600/**
601 * Retry a backed off syncing request immediately. This should only be used when
602 * the user <b>explicitly</b> attempts to retry their lost connection.
603 * @return {boolean} True if this resulted in a request being retried.
604 */
605SyncApi.prototype.retryImmediately = function () {
606 if (!this._connectionReturnedDefer) {
607 return false;
608 }
609 this._startKeepAlives(0);
610 return true;
611};
612/**
613 * Process a single set of cached sync data.
614 * @param {Object} savedSync a saved sync that was persisted by a store. This
615 * should have been acquired via client.store.getSavedSync().
616 */
617SyncApi.prototype._syncFromCache = function () {
618 var _ref4 = (0, _bluebird.coroutine)(_regenerator2.default.mark(function _callee4(savedSync) {
619 var nextSyncToken, syncEventData, data;
620 return _regenerator2.default.wrap(function _callee4$(_context4) {
621 while (1) {
622 switch (_context4.prev = _context4.next) {
623 case 0:
624 debuglog("sync(): not doing HTTP hit, instead returning stored /sync data");
625
626 nextSyncToken = savedSync.nextBatch;
627
628 // Set sync token for future incremental syncing
629
630 this.client.store.setSyncToken(nextSyncToken);
631
632 // No previous sync, set old token to null
633 syncEventData = {
634 oldSyncToken: null,
635 nextSyncToken: nextSyncToken,
636 catchingUp: false
637 };
638 data = {
639 next_batch: nextSyncToken,
640 rooms: savedSync.roomsData,
641 groups: savedSync.groupsData,
642 account_data: {
643 events: savedSync.accountData
644 }
645 };
646 _context4.prev = 5;
647 _context4.next = 8;
648 return (0, _bluebird.resolve)(this._processSyncResponse(syncEventData, data));
649
650 case 8:
651 _context4.next = 13;
652 break;
653
654 case 10:
655 _context4.prev = 10;
656 _context4.t0 = _context4["catch"](5);
657
658 console.error("Error processing cached sync", _context4.t0.stack || _context4.t0);
659
660 case 13:
661
662 this._updateSyncState("PREPARED", syncEventData);
663
664 case 14:
665 case "end":
666 return _context4.stop();
667 }
668 }
669 }, _callee4, this, [[5, 10]]);
670 }));
671
672 return function (_x3) {
673 return _ref4.apply(this, arguments);
674 };
675}();
676
677/**
678 * Invoke me to do /sync calls
679 * @param {Object} syncOptions
680 * @param {string} syncOptions.filterId
681 * @param {boolean} syncOptions.hasSyncedBefore
682 */
683SyncApi.prototype._sync = function () {
684 var _ref5 = (0, _bluebird.coroutine)(_regenerator2.default.mark(function _callee5(syncOptions) {
685 var client, syncToken, data, syncEventData;
686 return _regenerator2.default.wrap(function _callee5$(_context5) {
687 while (1) {
688 switch (_context5.prev = _context5.next) {
689 case 0:
690 client = this.client;
691
692 if (this._running) {
693 _context5.next = 6;
694 break;
695 }
696
697 debuglog("Sync no longer running: exiting.");
698 if (this._connectionReturnedDefer) {
699 this._connectionReturnedDefer.reject();
700 this._connectionReturnedDefer = null;
701 }
702 this._updateSyncState("STOPPED");
703 return _context5.abrupt("return");
704
705 case 6:
706 syncToken = client.store.getSyncToken();
707 data = void 0;
708 _context5.prev = 8;
709
710 //debuglog('Starting sync since=' + syncToken);
711 if (this._currentSyncRequest === null) {
712 this._currentSyncRequest = this._doSyncRequest(syncOptions, syncToken);
713 }
714 _context5.next = 12;
715 return (0, _bluebird.resolve)(this._currentSyncRequest);
716
717 case 12:
718 data = _context5.sent;
719 _context5.next = 19;
720 break;
721
722 case 15:
723 _context5.prev = 15;
724 _context5.t0 = _context5["catch"](8);
725
726 this._onSyncError(_context5.t0, syncOptions);
727 return _context5.abrupt("return");
728
729 case 19:
730 _context5.prev = 19;
731
732 this._currentSyncRequest = null;
733 return _context5.finish(19);
734
735 case 22:
736
737 //debuglog('Completed sync, next_batch=' + data.next_batch);
738
739 // set the sync token NOW *before* processing the events. We do this so
740 // if something barfs on an event we can skip it rather than constantly
741 // polling with the same token.
742 client.store.setSyncToken(data.next_batch);
743
744 // Reset after a successful sync
745 this._failedSyncCount = 0;
746
747 _context5.next = 26;
748 return (0, _bluebird.resolve)(client.store.setSyncData(data));
749
750 case 26:
751 syncEventData = {
752 oldSyncToken: syncToken,
753 nextSyncToken: data.next_batch,
754 catchingUp: this._catchingUp
755 };
756
757 if (!this.opts.crypto) {
758 _context5.next = 30;
759 break;
760 }
761
762 _context5.next = 30;
763 return (0, _bluebird.resolve)(this.opts.crypto.onSyncWillProcess(syncEventData));
764
765 case 30:
766 _context5.prev = 30;
767 _context5.next = 33;
768 return (0, _bluebird.resolve)(this._processSyncResponse(syncEventData, data));
769
770 case 33:
771 _context5.next = 38;
772 break;
773
774 case 35:
775 _context5.prev = 35;
776 _context5.t1 = _context5["catch"](30);
777
778 // log the exception with stack if we have it, else fall back
779 // to the plain description
780 console.error("Caught /sync error", _context5.t1.stack || _context5.t1);
781
782 case 38:
783
784 // update this as it may have changed
785 syncEventData.catchingUp = this._catchingUp;
786
787 // emit synced events
788 if (!syncOptions.hasSyncedBefore) {
789 this._updateSyncState("PREPARED", syncEventData);
790 syncOptions.hasSyncedBefore = true;
791 }
792
793 // tell the crypto module to do its processing. It may block (to do a
794 // /keys/changes request).
795
796 if (!this.opts.crypto) {
797 _context5.next = 43;
798 break;
799 }
800
801 _context5.next = 43;
802 return (0, _bluebird.resolve)(this.opts.crypto.onSyncCompleted(syncEventData));
803
804 case 43:
805
806 // keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
807 this._updateSyncState("SYNCING", syncEventData);
808
809 if (!client.store.wantsSave()) {
810 _context5.next = 49;
811 break;
812 }
813
814 if (!this.opts.crypto) {
815 _context5.next = 48;
816 break;
817 }
818
819 _context5.next = 48;
820 return (0, _bluebird.resolve)(this.opts.crypto.saveDeviceList(0));
821
822 case 48:
823
824 // tell databases that everything is now in a consistent state and can be saved.
825 client.store.save();
826
827 case 49:
828
829 // Begin next sync
830 this._sync(syncOptions);
831
832 case 50:
833 case "end":
834 return _context5.stop();
835 }
836 }
837 }, _callee5, this, [[8, 15, 19, 22], [30, 35]]);
838 }));
839
840 return function (_x4) {
841 return _ref5.apply(this, arguments);
842 };
843}();
844
845SyncApi.prototype._doSyncRequest = function (syncOptions, syncToken) {
846 var qps = this._getSyncParams(syncOptions, syncToken);
847 return this.client._http.authedRequest(undefined, "GET", "/sync", qps, undefined, qps.timeout + BUFFER_PERIOD_MS);
848};
849
850SyncApi.prototype._getSyncParams = function (syncOptions, syncToken) {
851 var pollTimeout = this.opts.pollTimeout;
852
853 if (this.getSyncState() !== 'SYNCING' || this._catchingUp) {
854 // unless we are happily syncing already, we want the server to return
855 // as quickly as possible, even if there are no events queued. This
856 // serves two purposes:
857 //
858 // * When the connection dies, we want to know asap when it comes back,
859 // so that we can hide the error from the user. (We don't want to
860 // have to wait for an event or a timeout).
861 //
862 // * We want to know if the server has any to_device messages queued up
863 // for us. We do that by calling it with a zero timeout until it
864 // doesn't give us any more to_device messages.
865 this._catchingUp = true;
866 pollTimeout = 0;
867 }
868
869 var filterId = syncOptions.filterId;
870 if (this.client.isGuest() && !filterId) {
871 filterId = this._getGuestFilter();
872 }
873
874 var qps = {
875 filter: filterId,
876 timeout: pollTimeout
877 };
878
879 if (this.opts.disablePresence) {
880 qps.set_presence = "offline";
881 }
882
883 if (syncToken) {
884 qps.since = syncToken;
885 } else {
886 // use a cachebuster for initialsyncs, to make sure that
887 // we don't get a stale sync
888 // (https://github.com/vector-im/vector-web/issues/1354)
889 qps._cacheBuster = Date.now();
890 }
891
892 if (this.getSyncState() == 'ERROR' || this.getSyncState() == 'RECONNECTING') {
893 // we think the connection is dead. If it comes back up, we won't know
894 // about it till /sync returns. If the timeout= is high, this could
895 // be a long time. Set it to 0 when doing retries so we don't have to wait
896 // for an event or a timeout before emiting the SYNCING event.
897 qps.timeout = 0;
898 }
899
900 return qps;
901};
902
903SyncApi.prototype._onSyncError = function (err, syncOptions) {
904 var _this = this;
905
906 if (!this._running) {
907 debuglog("Sync no longer running: exiting");
908 if (this._connectionReturnedDefer) {
909 this._connectionReturnedDefer.reject();
910 this._connectionReturnedDefer = null;
911 }
912 this._updateSyncState("STOPPED");
913 return;
914 }
915
916 console.error("/sync error %s", err);
917 console.error(err);
918
919 this._failedSyncCount++;
920 console.log('Number of consecutive failed sync requests:', this._failedSyncCount);
921
922 debuglog("Starting keep-alive");
923 // Note that we do *not* mark the sync connection as
924 // lost yet: we only do this if a keepalive poke
925 // fails, since long lived HTTP connections will
926 // go away sometimes and we shouldn't treat this as
927 // erroneous. We set the state to 'reconnecting'
928 // instead, so that clients can onserve this state
929 // if they wish.
930 this._startKeepAlives().then(function () {
931 _this._sync(syncOptions);
932 });
933
934 this._currentSyncRequest = null;
935 // Transition from RECONNECTING to ERROR after a given number of failed syncs
936 this._updateSyncState(this._failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ? "ERROR" : "RECONNECTING");
937};
938
939/**
940 * Process data returned from a sync response and propagate it
941 * into the model objects
942 *
943 * @param {Object} syncEventData Object containing sync tokens associated with this sync
944 * @param {Object} data The response from /sync
945 */
946SyncApi.prototype._processSyncResponse = function () {
947 var _ref6 = (0, _bluebird.coroutine)(_regenerator2.default.mark(function _callee8(syncEventData, data) {
948 var client, self, events, inviteRooms, joinRooms, leaveRooms, currentCount;
949 return _regenerator2.default.wrap(function _callee8$(_context8) {
950 while (1) {
951 switch (_context8.prev = _context8.next) {
952 case 0:
953 client = this.client;
954 self = this;
955
956 // data looks like:
957 // {
958 // next_batch: $token,
959 // presence: { events: [] },
960 // account_data: { events: [] },
961 // device_lists: { changed: ["@user:server", ... ]},
962 // to_device: { events: [] },
963 // device_one_time_keys_count: { signed_curve25519: 42 },
964 // rooms: {
965 // invite: {
966 // $roomid: {
967 // invite_state: { events: [] }
968 // }
969 // },
970 // join: {
971 // $roomid: {
972 // state: { events: [] },
973 // timeline: { events: [], prev_batch: $token, limited: true },
974 // ephemeral: { events: [] },
975 // account_data: { events: [] },
976 // unread_notifications: {
977 // highlight_count: 0,
978 // notification_count: 0,
979 // }
980 // }
981 // },
982 // leave: {
983 // $roomid: {
984 // state: { events: [] },
985 // timeline: { events: [], prev_batch: $token }
986 // }
987 // }
988 // },
989 // groups: {
990 // invite: {
991 // $groupId: {
992 // inviter: $inviter,
993 // profile: {
994 // avatar_url: $avatarUrl,
995 // name: $groupName,
996 // },
997 // },
998 // },
999 // join: {},
1000 // leave: {},
1001 // },
1002 // }
1003
1004 // TODO-arch:
1005 // - Each event we pass through needs to be emitted via 'event', can we
1006 // do this in one place?
1007 // - The isBrandNewRoom boilerplate is boilerplatey.
1008
1009 // handle presence events (User objects)
1010
1011 if (data.presence && utils.isArray(data.presence.events)) {
1012 data.presence.events.map(client.getEventMapper()).forEach(function (presenceEvent) {
1013 var user = client.store.getUser(presenceEvent.getSender());
1014 if (user) {
1015 user.setPresenceEvent(presenceEvent);
1016 } else {
1017 user = createNewUser(client, presenceEvent.getSender());
1018 user.setPresenceEvent(presenceEvent);
1019 client.store.storeUser(user);
1020 }
1021 client.emit("event", presenceEvent);
1022 });
1023 }
1024
1025 // handle non-room account_data
1026 if (data.account_data && utils.isArray(data.account_data.events)) {
1027 events = data.account_data.events.map(client.getEventMapper());
1028
1029 client.store.storeAccountDataEvents(events);
1030 events.forEach(function (accountDataEvent) {
1031 // Honour push rules that come down the sync stream but also
1032 // honour push rules that were previously cached. Base rules
1033 // will be updated when we recieve push rules via getPushRules
1034 // (see SyncApi.prototype.sync) before syncing over the network.
1035 if (accountDataEvent.getType() == 'm.push_rules') {
1036 client.pushRules = accountDataEvent.getContent();
1037 }
1038 client.emit("accountData", accountDataEvent);
1039 return accountDataEvent;
1040 });
1041 }
1042
1043 // handle to-device events
1044 if (data.to_device && utils.isArray(data.to_device.events) && data.to_device.events.length > 0) {
1045 data.to_device.events.map(client.getEventMapper()).forEach(function (toDeviceEvent) {
1046 var content = toDeviceEvent.getContent();
1047 if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") {
1048 // the mapper already logged a warning.
1049 console.log('Ignoring undecryptable to-device event from ' + toDeviceEvent.getSender());
1050 return;
1051 }
1052
1053 client.emit("toDeviceEvent", toDeviceEvent);
1054 });
1055 } else {
1056 // no more to-device events: we can stop polling with a short timeout.
1057 this._catchingUp = false;
1058 }
1059
1060 if (data.groups) {
1061 if (data.groups.invite) {
1062 this._processGroupSyncEntry(data.groups.invite, 'invite');
1063 }
1064
1065 if (data.groups.join) {
1066 this._processGroupSyncEntry(data.groups.join, 'join');
1067 }
1068
1069 if (data.groups.leave) {
1070 this._processGroupSyncEntry(data.groups.leave, 'leave');
1071 }
1072 }
1073
1074 // the returned json structure is a bit crap, so make it into a
1075 // nicer form (array) after applying sanity to make sure we don't fail
1076 // on missing keys (on the off chance)
1077 inviteRooms = [];
1078 joinRooms = [];
1079 leaveRooms = [];
1080
1081
1082 if (data.rooms) {
1083 if (data.rooms.invite) {
1084 inviteRooms = this._mapSyncResponseToRoomArray(data.rooms.invite);
1085 }
1086 if (data.rooms.join) {
1087 joinRooms = this._mapSyncResponseToRoomArray(data.rooms.join);
1088 }
1089 if (data.rooms.leave) {
1090 leaveRooms = this._mapSyncResponseToRoomArray(data.rooms.leave);
1091 }
1092 }
1093
1094 this._notifEvents = [];
1095
1096 // Handle invites
1097 inviteRooms.forEach(function (inviteObj) {
1098 var room = inviteObj.room;
1099 var stateEvents = self._mapSyncEventsFormat(inviteObj.invite_state, room);
1100 self._processRoomEvents(room, stateEvents);
1101 if (inviteObj.isBrandNewRoom) {
1102 room.recalculate(client.credentials.userId);
1103 client.store.storeRoom(room);
1104 client.emit("Room", room);
1105 }
1106 stateEvents.forEach(function (e) {
1107 client.emit("event", e);
1108 });
1109 });
1110
1111 // Handle joins
1112 _context8.next = 14;
1113 return (0, _bluebird.resolve)(_bluebird2.default.mapSeries(joinRooms, function () {
1114 var _ref7 = (0, _bluebird.coroutine)(_regenerator2.default.mark(function _callee7(joinObj) {
1115 var processRoomEvent = function () {
1116 var _ref8 = (0, _bluebird.coroutine)(_regenerator2.default.mark(function _callee6(e) {
1117 return _regenerator2.default.wrap(function _callee6$(_context6) {
1118 while (1) {
1119 switch (_context6.prev = _context6.next) {
1120 case 0:
1121 client.emit("event", e);
1122
1123 if (!(e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto)) {
1124 _context6.next = 4;
1125 break;
1126 }
1127
1128 _context6.next = 4;
1129 return (0, _bluebird.resolve)(self.opts.crypto.onCryptoEvent(e));
1130
1131 case 4:
1132 case "end":
1133 return _context6.stop();
1134 }
1135 }
1136 }, _callee6, this);
1137 }));
1138
1139 return function processRoomEvent(_x8) {
1140 return _ref8.apply(this, arguments);
1141 };
1142 }();
1143
1144 var room, stateEvents, timelineEvents, ephemeralEvents, accountDataEvents, limited, i, eventId;
1145 return _regenerator2.default.wrap(function _callee7$(_context7) {
1146 while (1) {
1147 switch (_context7.prev = _context7.next) {
1148 case 0:
1149 room = joinObj.room;
1150 stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
1151 timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room);
1152 ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral);
1153 accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data);
1154
1155 // we do this first so it's correct when any of the events fire
1156
1157 if (joinObj.unread_notifications) {
1158 room.setUnreadNotificationCount('total', joinObj.unread_notifications.notification_count);
1159 room.setUnreadNotificationCount('highlight', joinObj.unread_notifications.highlight_count);
1160 }
1161
1162 joinObj.timeline = joinObj.timeline || {};
1163
1164 if (!joinObj.isBrandNewRoom) {
1165 _context7.next = 11;
1166 break;
1167 }
1168
1169 // set the back-pagination token. Do this *before* adding any
1170 // events so that clients can start back-paginating.
1171 room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
1172 _context7.next = 25;
1173 break;
1174
1175 case 11:
1176 if (!joinObj.timeline.limited) {
1177 _context7.next = 25;
1178 break;
1179 }
1180
1181 limited = true;
1182
1183 // we've got a limited sync, so we *probably* have a gap in the
1184 // timeline, so should reset. But we might have been peeking or
1185 // paginating and already have some of the events, in which
1186 // case we just want to append any subsequent events to the end
1187 // of the existing timeline.
1188 //
1189 // This is particularly important in the case that we already have
1190 // *all* of the events in the timeline - in that case, if we reset
1191 // the timeline, we'll end up with an entirely empty timeline,
1192 // which we'll try to paginate but not get any new events (which
1193 // will stop us linking the empty timeline into the chain).
1194 //
1195
1196 i = timelineEvents.length - 1;
1197
1198 case 14:
1199 if (!(i >= 0)) {
1200 _context7.next = 24;
1201 break;
1202 }
1203
1204 eventId = timelineEvents[i].getId();
1205
1206 if (!room.getTimelineForEvent(eventId)) {
1207 _context7.next = 21;
1208 break;
1209 }
1210
1211 debuglog("Already have event " + eventId + " in limited " + "sync - not resetting");
1212 limited = false;
1213
1214 // we might still be missing some of the events before i;
1215 // we don't want to be adding them to the end of the
1216 // timeline because that would put them out of order.
1217 timelineEvents.splice(0, i);
1218
1219 // XXX: there's a problem here if the skipped part of the
1220 // timeline modifies the state set in stateEvents, because
1221 // we'll end up using the state from stateEvents rather
1222 // than the later state from timelineEvents. We probably
1223 // need to wind stateEvents forward over the events we're
1224 // skipping.
1225
1226 return _context7.abrupt("break", 24);
1227
1228 case 21:
1229 i--;
1230 _context7.next = 14;
1231 break;
1232
1233 case 24:
1234
1235 if (limited) {
1236 self._deregisterStateListeners(room);
1237 room.resetLiveTimeline(joinObj.timeline.prev_batch, self.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken);
1238
1239 // We have to assume any gap in any timeline is
1240 // reason to stop incrementally tracking notifications and
1241 // reset the timeline.
1242 client.resetNotifTimelineSet();
1243
1244 self._registerStateListeners(room);
1245 }
1246
1247 case 25:
1248
1249 self._processRoomEvents(room, stateEvents, timelineEvents);
1250
1251 // XXX: should we be adding ephemeralEvents to the timeline?
1252 // It feels like that for symmetry with room.addAccountData()
1253 // there should be a room.addEphemeralEvents() or similar.
1254 room.addLiveEvents(ephemeralEvents);
1255
1256 // we deliberately don't add accountData to the timeline
1257 room.addAccountData(accountDataEvents);
1258
1259 room.recalculate(client.credentials.userId);
1260 if (joinObj.isBrandNewRoom) {
1261 client.store.storeRoom(room);
1262 client.emit("Room", room);
1263 }
1264
1265 self._processEventsForNotifs(room, timelineEvents);
1266
1267 _context7.next = 33;
1268 return (0, _bluebird.resolve)(_bluebird2.default.mapSeries(stateEvents, processRoomEvent));
1269
1270 case 33:
1271 _context7.next = 35;
1272 return (0, _bluebird.resolve)(_bluebird2.default.mapSeries(timelineEvents, processRoomEvent));
1273
1274 case 35:
1275 ephemeralEvents.forEach(function (e) {
1276 client.emit("event", e);
1277 });
1278 accountDataEvents.forEach(function (e) {
1279 client.emit("event", e);
1280 });
1281
1282 case 37:
1283 case "end":
1284 return _context7.stop();
1285 }
1286 }
1287 }, _callee7, this);
1288 }));
1289
1290 return function (_x7) {
1291 return _ref7.apply(this, arguments);
1292 };
1293 }()));
1294
1295 case 14:
1296
1297 // Handle leaves (e.g. kicked rooms)
1298 leaveRooms.forEach(function (leaveObj) {
1299 var room = leaveObj.room;
1300 var stateEvents = self._mapSyncEventsFormat(leaveObj.state, room);
1301 var timelineEvents = self._mapSyncEventsFormat(leaveObj.timeline, room);
1302 var accountDataEvents = self._mapSyncEventsFormat(leaveObj.account_data);
1303
1304 self._processRoomEvents(room, stateEvents, timelineEvents);
1305 room.addAccountData(accountDataEvents);
1306
1307 room.recalculate(client.credentials.userId);
1308 if (leaveObj.isBrandNewRoom) {
1309 client.store.storeRoom(room);
1310 client.emit("Room", room);
1311 }
1312
1313 self._processEventsForNotifs(room, timelineEvents);
1314
1315 stateEvents.forEach(function (e) {
1316 client.emit("event", e);
1317 });
1318 timelineEvents.forEach(function (e) {
1319 client.emit("event", e);
1320 });
1321 accountDataEvents.forEach(function (e) {
1322 client.emit("event", e);
1323 });
1324 });
1325
1326 // update the notification timeline, if appropriate.
1327 // we only do this for live events, as otherwise we can't order them sanely
1328 // in the timeline relative to ones paginated in by /notifications.
1329 // XXX: we could fix this by making EventTimeline support chronological
1330 // ordering... but it doesn't, right now.
1331 if (syncEventData.oldSyncToken && this._notifEvents.length) {
1332 this._notifEvents.sort(function (a, b) {
1333 return a.getTs() - b.getTs();
1334 });
1335 this._notifEvents.forEach(function (event) {
1336 client.getNotifTimelineSet().addLiveEvent(event);
1337 });
1338 }
1339
1340 // Handle device list updates
1341
1342 if (!data.device_lists) {
1343 _context8.next = 22;
1344 break;
1345 }
1346
1347 if (!this.opts.crypto) {
1348 _context8.next = 22;
1349 break;
1350 }
1351
1352 _context8.next = 20;
1353 return (0, _bluebird.resolve)(this.opts.crypto.handleDeviceListChanges(syncEventData, data.device_lists));
1354
1355 case 20:
1356 _context8.next = 22;
1357 break;
1358
1359 case 22:
1360
1361 // Handle one_time_keys_count
1362 if (this.opts.crypto && data.device_one_time_keys_count) {
1363 currentCount = data.device_one_time_keys_count.signed_curve25519 || 0;
1364
1365 this.opts.crypto.updateOneTimeKeyCount(currentCount);
1366 }
1367
1368 case 23:
1369 case "end":
1370 return _context8.stop();
1371 }
1372 }
1373 }, _callee8, this);
1374 }));
1375
1376 return function (_x5, _x6) {
1377 return _ref6.apply(this, arguments);
1378 };
1379}();
1380
1381/**
1382 * Starts polling the connectivity check endpoint
1383 * @param {number} delay How long to delay until the first poll.
1384 * defaults to a short, randomised interval (to prevent
1385 * tightlooping if /versions succeeds but /sync etc. fail).
1386 * @return {promise} which resolves once the connection returns
1387 */
1388SyncApi.prototype._startKeepAlives = function (delay) {
1389 if (delay === undefined) {
1390 delay = 2000 + Math.floor(Math.random() * 5000);
1391 }
1392
1393 if (this._keepAliveTimer !== null) {
1394 clearTimeout(this._keepAliveTimer);
1395 }
1396 var self = this;
1397 if (delay > 0) {
1398 self._keepAliveTimer = setTimeout(self._pokeKeepAlive.bind(self), delay);
1399 } else {
1400 self._pokeKeepAlive();
1401 }
1402 if (!this._connectionReturnedDefer) {
1403 this._connectionReturnedDefer = _bluebird2.default.defer();
1404 }
1405 return this._connectionReturnedDefer.promise;
1406};
1407
1408/**
1409 * Make a dummy call to /_matrix/client/versions, to see if the HS is
1410 * reachable.
1411 *
1412 * On failure, schedules a call back to itself. On success, resolves
1413 * this._connectionReturnedDefer.
1414 */
1415SyncApi.prototype._pokeKeepAlive = function () {
1416 var self = this;
1417 function success() {
1418 clearTimeout(self._keepAliveTimer);
1419 if (self._connectionReturnedDefer) {
1420 self._connectionReturnedDefer.resolve();
1421 self._connectionReturnedDefer = null;
1422 }
1423 }
1424
1425 this.client._http.request(undefined, // callback
1426 "GET", "/_matrix/client/versions", undefined, // queryParams
1427 undefined, // data
1428 {
1429 prefix: '',
1430 localTimeoutMs: 15 * 1000
1431 }).done(function () {
1432 success();
1433 }, function (err) {
1434 if (err.httpStatus == 400) {
1435 // treat this as a success because the server probably just doesn't
1436 // support /versions: point is, we're getting a response.
1437 // We wait a short time though, just in case somehow the server
1438 // is in a mode where it 400s /versions responses and sync etc.
1439 // responses fail, this will mean we don't hammer in a loop.
1440 self._keepAliveTimer = setTimeout(success, 2000);
1441 } else {
1442 self._keepAliveTimer = setTimeout(self._pokeKeepAlive.bind(self), 5000 + Math.floor(Math.random() * 5000));
1443 // A keepalive has failed, so we emit the
1444 // error state (whether or not this is the
1445 // first failure).
1446 // Note we do this after setting the timer:
1447 // this lets the unit tests advance the mock
1448 // clock when the get the error.
1449 self._updateSyncState("ERROR", { error: err });
1450 }
1451 });
1452};
1453
1454/**
1455 * @param {Object} groupsSection Groups section object, eg. response.groups.invite
1456 * @param {string} sectionName Which section this is ('invite', 'join' or 'leave')
1457 */
1458SyncApi.prototype._processGroupSyncEntry = function (groupsSection, sectionName) {
1459 // Processes entries from 'groups' section of the sync stream
1460 var _iteratorNormalCompletion = true;
1461 var _didIteratorError = false;
1462 var _iteratorError = undefined;
1463
1464 try {
1465 for (var _iterator = (0, _getIterator3.default)((0, _keys2.default)(groupsSection)), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
1466 var groupId = _step.value;
1467
1468 var groupInfo = groupsSection[groupId];
1469 var group = this.client.store.getGroup(groupId);
1470 var isBrandNew = group === null;
1471 if (group === null) {
1472 group = this.createGroup(groupId);
1473 }
1474 if (groupInfo.profile) {
1475 group.setProfile(groupInfo.profile.name, groupInfo.profile.avatar_url);
1476 }
1477 if (groupInfo.inviter) {
1478 group.setInviter({ userId: groupInfo.inviter });
1479 }
1480 group.setMyMembership(sectionName);
1481 if (isBrandNew) {
1482 // Now we've filled in all the fields, emit the Group event
1483 this.client.emit("Group", group);
1484 }
1485 }
1486 } catch (err) {
1487 _didIteratorError = true;
1488 _iteratorError = err;
1489 } finally {
1490 try {
1491 if (!_iteratorNormalCompletion && _iterator.return) {
1492 _iterator.return();
1493 }
1494 } finally {
1495 if (_didIteratorError) {
1496 throw _iteratorError;
1497 }
1498 }
1499 }
1500};
1501
1502/**
1503 * @param {Object} obj
1504 * @return {Object[]}
1505 */
1506SyncApi.prototype._mapSyncResponseToRoomArray = function (obj) {
1507 // Maps { roomid: {stuff}, roomid: {stuff} }
1508 // to
1509 // [{stuff+Room+isBrandNewRoom}, {stuff+Room+isBrandNewRoom}]
1510 var client = this.client;
1511 var self = this;
1512 return utils.keys(obj).map(function (roomId) {
1513 var arrObj = obj[roomId];
1514 var room = client.store.getRoom(roomId);
1515 var isBrandNewRoom = false;
1516 if (!room) {
1517 room = self.createRoom(roomId);
1518 isBrandNewRoom = true;
1519 }
1520 arrObj.room = room;
1521 arrObj.isBrandNewRoom = isBrandNewRoom;
1522 return arrObj;
1523 });
1524};
1525
1526/**
1527 * @param {Object} obj
1528 * @param {Room} room
1529 * @return {MatrixEvent[]}
1530 */
1531SyncApi.prototype._mapSyncEventsFormat = function (obj, room) {
1532 if (!obj || !utils.isArray(obj.events)) {
1533 return [];
1534 }
1535 var mapper = this.client.getEventMapper();
1536 return obj.events.map(function (e) {
1537 if (room) {
1538 e.room_id = room.roomId;
1539 }
1540 return mapper(e);
1541 });
1542};
1543
1544/**
1545 * @param {Room} room
1546 */
1547SyncApi.prototype._resolveInvites = function (room) {
1548 if (!room || !this.opts.resolveInvitesToProfiles) {
1549 return;
1550 }
1551 var client = this.client;
1552 // For each invited room member we want to give them a displayname/avatar url
1553 // if they have one (the m.room.member invites don't contain this).
1554 room.getMembersWithMembership("invite").forEach(function (member) {
1555 if (member._requestedProfileInfo) {
1556 return;
1557 }
1558 member._requestedProfileInfo = true;
1559 // try to get a cached copy first.
1560 var user = client.getUser(member.userId);
1561 var promise = void 0;
1562 if (user) {
1563 promise = _bluebird2.default.resolve({
1564 avatar_url: user.avatarUrl,
1565 displayname: user.displayName
1566 });
1567 } else {
1568 promise = client.getProfileInfo(member.userId);
1569 }
1570 promise.done(function (info) {
1571 // slightly naughty by doctoring the invite event but this means all
1572 // the code paths remain the same between invite/join display name stuff
1573 // which is a worthy trade-off for some minor pollution.
1574 var inviteEvent = member.events.member;
1575 if (inviteEvent.getContent().membership !== "invite") {
1576 // between resolving and now they have since joined, so don't clobber
1577 return;
1578 }
1579 inviteEvent.getContent().avatar_url = info.avatar_url;
1580 inviteEvent.getContent().displayname = info.displayname;
1581 // fire listeners
1582 member.setMembershipEvent(inviteEvent, room.currentState);
1583 }, function (err) {
1584 // OH WELL.
1585 });
1586 });
1587};
1588
1589/**
1590 * @param {Room} room
1591 * @param {MatrixEvent[]} stateEventList A list of state events. This is the state
1592 * at the *START* of the timeline list if it is supplied.
1593 * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
1594 * is earlier in time. Higher index is later.
1595 */
1596SyncApi.prototype._processRoomEvents = function (room, stateEventList, timelineEventList) {
1597 // If there are no events in the timeline yet, initialise it with
1598 // the given state events
1599 var liveTimeline = room.getLiveTimeline();
1600 var timelineWasEmpty = liveTimeline.getEvents().length == 0;
1601 if (timelineWasEmpty) {
1602 // Passing these events into initialiseState will freeze them, so we need
1603 // to compute and cache the push actions for them now, otherwise sync dies
1604 // with an attempt to assign to read only property.
1605 // XXX: This is pretty horrible and is assuming all sorts of behaviour from
1606 // these functions that it shouldn't be. We should probably either store the
1607 // push actions cache elsewhere so we can freeze MatrixEvents, or otherwise
1608 // find some solution where MatrixEvents are immutable but allow for a cache
1609 // field.
1610 var _iteratorNormalCompletion2 = true;
1611 var _didIteratorError2 = false;
1612 var _iteratorError2 = undefined;
1613
1614 try {
1615 for (var _iterator2 = (0, _getIterator3.default)(stateEventList), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
1616 var ev = _step2.value;
1617
1618 this.client.getPushActionsForEvent(ev);
1619 }
1620 } catch (err) {
1621 _didIteratorError2 = true;
1622 _iteratorError2 = err;
1623 } finally {
1624 try {
1625 if (!_iteratorNormalCompletion2 && _iterator2.return) {
1626 _iterator2.return();
1627 }
1628 } finally {
1629 if (_didIteratorError2) {
1630 throw _iteratorError2;
1631 }
1632 }
1633 }
1634
1635 liveTimeline.initialiseState(stateEventList);
1636 }
1637
1638 this._resolveInvites(room);
1639
1640 // recalculate the room name at this point as adding events to the timeline
1641 // may make notifications appear which should have the right name.
1642 // XXX: This looks suspect: we'll end up recalculating the room once here
1643 // and then again after adding events (_processSyncResponse calls it after
1644 // calling us) even if no state events were added. It also means that if
1645 // one of the room events in timelineEventList is something that needs
1646 // a recalculation (like m.room.name) we won't recalculate until we've
1647 // finished adding all the events, which will cause the notification to have
1648 // the old room name rather than the new one.
1649 room.recalculate(this.client.credentials.userId);
1650
1651 // If the timeline wasn't empty, we process the state events here: they're
1652 // defined as updates to the state before the start of the timeline, so this
1653 // starts to roll the state forward.
1654 // XXX: That's what we *should* do, but this can happen if we were previously
1655 // peeking in a room, in which case we obviously do *not* want to add the
1656 // state events here onto the end of the timeline. Historically, the js-sdk
1657 // has just set these new state events on the old and new state. This seems
1658 // very wrong because there could be events in the timeline that diverge the
1659 // state, in which case this is going to leave things out of sync. However,
1660 // for now I think it;s best to behave the same as the code has done previously.
1661 if (!timelineWasEmpty) {
1662 // XXX: As above, don't do this...
1663 //room.addLiveEvents(stateEventList || []);
1664 // Do this instead...
1665 room.oldState.setStateEvents(stateEventList || []);
1666 room.currentState.setStateEvents(stateEventList || []);
1667 }
1668 // execute the timeline events. This will continue to diverge the current state
1669 // if the timeline has any state events in it.
1670 // This also needs to be done before running push rules on the events as they need
1671 // to be decorated with sender etc.
1672 room.addLiveEvents(timelineEventList || []);
1673};
1674
1675/**
1676 * Takes a list of timelineEvents and adds and adds to _notifEvents
1677 * as appropriate.
1678 * This must be called after the room the events belong to has been stored.
1679 *
1680 * @param {Room} room
1681 * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
1682 * is earlier in time. Higher index is later.
1683 */
1684SyncApi.prototype._processEventsForNotifs = function (room, timelineEventList) {
1685 // gather our notifications into this._notifEvents
1686 if (this.client.getNotifTimelineSet()) {
1687 for (var i = 0; i < timelineEventList.length; i++) {
1688 var pushActions = this.client.getPushActionsForEvent(timelineEventList[i]);
1689 if (pushActions && pushActions.notify && pushActions.tweaks && pushActions.tweaks.highlight) {
1690 this._notifEvents.push(timelineEventList[i]);
1691 }
1692 }
1693 }
1694};
1695
1696/**
1697 * @return {string}
1698 */
1699SyncApi.prototype._getGuestFilter = function () {
1700 var guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching
1701 if (!guestRooms) {
1702 return "{}";
1703 }
1704 // we just need to specify the filter inline if we're a guest because guests
1705 // can't create filters.
1706 return (0, _stringify2.default)({
1707 room: {
1708 timeline: {
1709 limit: 20
1710 }
1711 }
1712 });
1713};
1714
1715/**
1716 * Sets the sync state and emits an event to say so
1717 * @param {String} newState The new state string
1718 * @param {Object} data Object of additional data to emit in the event
1719 */
1720SyncApi.prototype._updateSyncState = function (newState, data) {
1721 var old = this._syncState;
1722 this._syncState = newState;
1723 this.client.emit("sync", this._syncState, old, data);
1724};
1725
1726/**
1727 * Event handler for the 'online' event
1728 * This event is generally unreliable and precise behaviour
1729 * varies between browsers, so we poll for connectivity too,
1730 * but this might help us reconnect a little faster.
1731 */
1732SyncApi.prototype._onOnline = function () {
1733 debuglog("Browser thinks we are back online");
1734 this._startKeepAlives(0);
1735};
1736
1737function createNewUser(client, userId) {
1738 var user = new User(userId);
1739 client.reEmitter.reEmit(user, ["User.avatarUrl", "User.displayName", "User.presence", "User.currentlyActive", "User.lastPresenceTs"]);
1740 return user;
1741}
1742
1743/** */
1744module.exports = SyncApi;
1745//# sourceMappingURL=sync.js.map
\No newline at end of file