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