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 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
50 |
|
51 | var User = require("./models/user");
|
52 | var Room = require("./models/room");
|
53 | var Group = require('./models/group');
|
54 | var utils = require("./utils");
|
55 | var Filter = require("./filter");
|
56 | var EventTimeline = require("./models/event-timeline");
|
57 |
|
58 | var 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.
|
64 | var 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.
|
69 | var FAILED_SYNC_ERROR_THRESHOLD = 3;
|
70 |
|
71 | function 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 |
|
77 | function 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 | */
|
101 | function 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 | */
|
133 | SyncApi.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 | */
|
148 | SyncApi.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 | */
|
160 | SyncApi.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 | */
|
176 | SyncApi.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 | */
|
187 | SyncApi.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 | */
|
250 | SyncApi.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 | */
|
316 | SyncApi.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 | */
|
325 | SyncApi.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 | */
|
385 | SyncApi.prototype.getSyncState = function () {
|
386 | return this._syncState;
|
387 | };
|
388 |
|
389 | SyncApi.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 | */
|
422 | SyncApi.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 | */
|
584 | SyncApi.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 | */
|
605 | SyncApi.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 | */
|
617 | SyncApi.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 | */
|
683 | SyncApi.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 |
|
845 | SyncApi.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 |
|
850 | SyncApi.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 |
|
903 | SyncApi.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 | */
|
946 | SyncApi.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 | */
|
1388 | SyncApi.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 | */
|
1415 | SyncApi.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 | */
|
1458 | SyncApi.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 | */
|
1506 | SyncApi.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 | */
|
1531 | SyncApi.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 | */
|
1547 | SyncApi.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 | */
|
1596 | SyncApi.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 | */
|
1684 | SyncApi.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 | */
|
1699 | SyncApi.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 | */
|
1720 | SyncApi.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 | */
|
1732 | SyncApi.prototype._onOnline = function () {
|
1733 | debuglog("Browser thinks we are back online");
|
1734 | this._startKeepAlives(0);
|
1735 | };
|
1736 |
|
1737 | function 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 | /** */
|
1744 | module.exports = SyncApi;
|
1745 | //# sourceMappingURL=sync.js.map |
\ | No newline at end of file |