1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-29 16:43:09 +03:00

Merge branch 'develop' into markjh/megolm

Conflicts:
	lib/crypto/algorithms/megolm.js
This commit is contained in:
Mark Haines
2016-09-15 17:10:02 +01:00
19 changed files with 1673 additions and 687 deletions

View File

@@ -146,6 +146,7 @@ function MatrixClient(opts) {
this._ongoingScrollbacks = {}; this._ongoingScrollbacks = {};
this.timelineSupport = Boolean(opts.timelineSupport); this.timelineSupport = Boolean(opts.timelineSupport);
this.urlPreviewCache = {}; this.urlPreviewCache = {};
this._notifTimelineSet = null;
this._crypto = null; this._crypto = null;
if (CRYPTO_ENABLED && opts.sessionStore !== null && if (CRYPTO_ENABLED && opts.sessionStore !== null &&
@@ -249,6 +250,24 @@ MatrixClient.prototype.retryImmediately = function() {
return this._syncApi.retryImmediately(); return this._syncApi.retryImmediately();
}; };
/**
* Return the global notification EventTimelineSet, if any
*
* @return {EventTimelineSet} the globl notification EventTimelineSet
*/
MatrixClient.prototype.getNotifTimelineSet = function() {
return this._notifTimelineSet;
};
/**
* Set the global notification EventTimelineSet
*
* @param {EventTimelineSet} notifTimelineSet
*/
MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
this._notifTimelineSet = notifTimelineSet;
};
// Crypto bits // Crypto bits
// =========== // ===========
@@ -1330,16 +1349,16 @@ function _membershipChange(client, roomId, userId, membership, reason, callback)
/** /**
* Obtain a dict of actions which should be performed for this event according * Obtain a dict of actions which should be performed for this event according
* to the push rules for this user. * to the push rules for this user. Caches the dict on the event.
* @param {MatrixEvent} event The event to get push actions for. * @param {MatrixEvent} event The event to get push actions for.
* @return {module:pushprocessor~PushAction} A dict of actions to perform. * @return {module:pushprocessor~PushAction} A dict of actions to perform.
*/ */
MatrixClient.prototype.getPushActionsForEvent = function(event) { MatrixClient.prototype.getPushActionsForEvent = function(event) {
if (event._pushActions === undefined) { if (!event.getPushActions()) {
var pushProcessor = new PushProcessor(this); var pushProcessor = new PushProcessor(this);
event._pushActions = pushProcessor.actionsForEvent(event); event.setPushActions(pushProcessor.actionsForEvent(event));
} }
return event._pushActions; return event.getPushActions();
}; };
// Profile operations // Profile operations
@@ -1584,18 +1603,18 @@ MatrixClient.prototype.paginateEventContext = function(eventContext, opts) {
/** /**
* Get an EventTimeline for the given event * Get an EventTimeline for the given event
* *
* <p>If the room object already has the given event in its store, the * <p>If the EventTimelineSet object already has the given event in its store, the
* corresponding timeline will be returned. Otherwise, a /context request is * corresponding timeline will be returned. Otherwise, a /context request is
* made, and used to construct an EventTimeline. * made, and used to construct an EventTimeline.
* *
* @param {Room} room The room to look for the event in * @param {EventTimelineSet} timelineSet The timelineSet to look for the event in
* @param {string} eventId The ID of the event to look for * @param {string} eventId The ID of the event to look for
* *
* @return {module:client.Promise} Resolves: * @return {module:client.Promise} Resolves:
* {@link module:models/event-timeline~EventTimeline} including the given * {@link module:models/event-timeline~EventTimeline} including the given
* event * event
*/ */
MatrixClient.prototype.getEventTimeline = function(room, eventId) { MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
// don't allow any timeline support unless it's been enabled. // don't allow any timeline support unless it's been enabled.
if (!this.timelineSupport) { if (!this.timelineSupport) {
throw new Error("timeline support is disabled. Set the 'timelineSupport'" + throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
@@ -1603,13 +1622,13 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
" it."); " it.");
} }
if (room.getTimelineForEvent(eventId)) { if (timelineSet.getTimelineForEvent(eventId)) {
return q(room.getTimelineForEvent(eventId)); return q(timelineSet.getTimelineForEvent(eventId));
} }
var path = utils.encodeUri( var path = utils.encodeUri(
"/rooms/$roomId/context/$eventId", { "/rooms/$roomId/context/$eventId", {
$roomId: room.roomId, $roomId: timelineSet.room.roomId,
$eventId: eventId, $eventId: eventId,
} }
); );
@@ -1626,8 +1645,8 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
// by the time the request completes, the event might have ended up in // by the time the request completes, the event might have ended up in
// the timeline. // the timeline.
if (room.getTimelineForEvent(eventId)) { if (timelineSet.getTimelineForEvent(eventId)) {
return room.getTimelineForEvent(eventId); return timelineSet.getTimelineForEvent(eventId);
} }
// we start with the last event, since that's the point at which we // we start with the last event, since that's the point at which we
@@ -1639,21 +1658,22 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
.concat(res.events_before); .concat(res.events_before);
var matrixEvents = utils.map(events, self.getEventMapper()); var matrixEvents = utils.map(events, self.getEventMapper());
var timeline = room.getTimelineForEvent(matrixEvents[0].getId()); var timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId());
if (!timeline) { if (!timeline) {
timeline = room.addTimeline(); timeline = timelineSet.addTimeline();
timeline.initialiseState(utils.map(res.state, timeline.initialiseState(utils.map(res.state,
self.getEventMapper())); self.getEventMapper()));
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end; timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
} }
room.addEventsToTimeline(matrixEvents, true, timeline, res.start); timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start);
// there is no guarantee that the event ended up in "timeline" (we // there is no guarantee that the event ended up in "timeline" (we
// might have switched to a neighbouring timeline) - so check the // might have switched to a neighbouring timeline) - so check the
// room's index again. On the other hand, there's no guarantee the // room's index again. On the other hand, there's no guarantee the
// event ended up anywhere, if it was later redacted, so we just // event ended up anywhere, if it was later redacted, so we just
// return the timeline we first thought of. // return the timeline we first thought of.
return room.getTimelineForEvent(eventId) || timeline; var tl = timelineSet.getTimelineForEvent(eventId) || timeline;
return tl;
}); });
return promise; return promise;
}; };
@@ -1665,7 +1685,7 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
* @param {module:models/event-timeline~EventTimeline} eventTimeline timeline * @param {module:models/event-timeline~EventTimeline} eventTimeline timeline
* object to be updated * object to be updated
* @param {Object} [opts] * @param {Object} [opts]
* @param {boolean} [opts.backwards = false] true to fill backwards, * @param {bool} [opts.backwards = false] true to fill backwards,
* false to go forwards * false to go forwards
* @param {number} [opts.limit = 30] number of events to request * @param {number} [opts.limit = 30] number of events to request
* *
@@ -1673,14 +1693,17 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
* events and we reached either end of the timeline; else true. * events and we reached either end of the timeline; else true.
*/ */
MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) { MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
var isNotifTimeline = (eventTimeline.getTimelineSet() === this._notifTimelineSet);
// TODO: we should implement a backoff (as per scrollback()) to deal more // TODO: we should implement a backoff (as per scrollback()) to deal more
// nicely with HTTP errors. // nicely with HTTP errors.
opts = opts || {}; opts = opts || {};
var backwards = opts.backwards || false; var backwards = opts.backwards || false;
var room = this.getRoom(eventTimeline.getRoomId()); if (isNotifTimeline) {
if (!room) { if (!backwards) {
throw new Error("Unknown room " + eventTimeline.getRoomId()); throw new Error("paginateNotifTimeline can only paginate backwards");
}
} }
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
@@ -1698,23 +1721,81 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
return pendingRequest; return pendingRequest;
} }
var path = utils.encodeUri( var path, params, promise;
var self = this;
if (isNotifTimeline) {
path = "/notifications";
params = {
limit: ('limit' in opts) ? opts.limit : 30,
only: 'highlight',
};
if (token && token !== "end") {
params.from = token;
}
promise =
this._http.authedRequestWithPrefix(undefined, "GET", path, params,
undefined, httpApi.PREFIX_UNSTABLE
).then(function(res) {
var token = res.next_token;
var matrixEvents = [];
for (var i = 0; i < res.notifications.length; i++) {
var notification = res.notifications[i];
var event = self.getEventMapper()(notification.event);
event.setPushActions(
PushProcessor.actionListToActionsObject(notification.actions)
);
event.event.room_id = notification.room_id; // XXX: gutwrenching
matrixEvents[i] = event;
}
eventTimeline.getTimelineSet()
.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
// if we've hit the end of the timeline, we need to stop trying to
// paginate. We need to keep the 'forwards' token though, to make sure
// we can recover from gappy syncs.
if (backwards && !res.next_token) {
eventTimeline.setPaginationToken(null, dir);
}
return res.next_token ? true : false;
}).finally(function() {
eventTimeline._paginationRequests[dir] = null;
});
eventTimeline._paginationRequests[dir] = promise;
}
else {
var room = this.getRoom(eventTimeline.getRoomId());
if (!room) {
throw new Error("Unknown room " + eventTimeline.getRoomId());
}
path = utils.encodeUri(
"/rooms/$roomId/messages", {$roomId: eventTimeline.getRoomId()} "/rooms/$roomId/messages", {$roomId: eventTimeline.getRoomId()}
); );
var params = { params = {
from: token, from: token,
limit: ('limit' in opts) ? opts.limit : 30, limit: ('limit' in opts) ? opts.limit : 30,
dir: dir dir: dir
}; };
var self = this; var filter = eventTimeline.getFilter();
if (filter) {
// XXX: it's horrific that /messages' filter parameter doesn't match
// /sync's one - see https://matrix.org/jira/browse/SPEC-451
params.filter = JSON.stringify(filter.getRoomTimelineFilterComponent());
}
var promise = promise =
this._http.authedRequest(undefined, "GET", path, params this._http.authedRequest(undefined, "GET", path, params
).then(function(res) { ).then(function(res) {
var token = res.end; var token = res.end;
var matrixEvents = utils.map(res.chunk, self.getEventMapper()); var matrixEvents = utils.map(res.chunk, self.getEventMapper());
room.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); eventTimeline.getTimelineSet()
.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
// if we've hit the end of the timeline, we need to stop trying to // if we've hit the end of the timeline, we need to stop trying to
// paginate. We need to keep the 'forwards' token though, to make sure // paginate. We need to keep the 'forwards' token though, to make sure
@@ -1727,10 +1808,44 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
eventTimeline._paginationRequests[dir] = null; eventTimeline._paginationRequests[dir] = null;
}); });
eventTimeline._paginationRequests[dir] = promise; eventTimeline._paginationRequests[dir] = promise;
}
return promise; return promise;
}; };
/**
* Reset the notifTimelineSet entirely, paginating in some historical notifs as
* a starting point for subsequent pagination.
*/
MatrixClient.prototype.resetNotifTimelineSet = function() {
if (!this._notifTimelineSet) {
return;
}
// FIXME: This thing is a total hack, and results in duplicate events being
// added to the timeline both from /sync and /notifications, and lots of
// slow and wasteful processing and pagination. The correct solution is to
// extend /messages or /search or something to filter on notifications.
// use the fictitious token 'end'. in practice we would ideally give it
// the oldest backwards pagination token from /sync, but /sync doesn't
// know about /notifications, so we have no choice but to start paginating
// from the current point in time. This may well overlap with historical
// notifs which are then inserted into the timeline by /sync responses.
this._notifTimelineSet.resetLiveTimeline('end', true);
// we could try to paginate a single event at this point in order to get
// a more valid pagination token, but it just ends up with an out of order
// timeline. given what a mess this is and given we're going to have duplicate
// events anyway, just leave it with the dummy token for now.
/*
this.paginateNotifTimeline(this._notifTimelineSet.getLiveTimeline(), {
backwards: true,
limit: 1
});
*/
};
/** /**
* Peek into a room and receive updates about the room. This only works if the * Peek into a room and receive updates about the room. This only works if the
* history visibility for the room is world_readable. * history visibility for the room is world_readable.
@@ -2242,6 +2357,54 @@ MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
}); });
}; };
/**
* @param {string} filterName
* @param {Filter} filter
* @return {Promise<String>} Filter ID
*/
MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
var filterId = this.store.getFilterIdByName(filterName);
var promise = q();
var self = this;
if (filterId) {
// check that the existing filter matches our expectations
promise = self.getFilter(self.credentials.userId,
filterId, true
).then(function(existingFilter) {
var oldDef = existingFilter.getDefinition();
var newDef = filter.getDefinition();
if (utils.deepCompare(oldDef, newDef)) {
// super, just use that.
// debuglog("Using existing filter ID %s: %s", filterId,
// JSON.stringify(oldDef));
return q(filterId);
}
// debuglog("Existing filter ID %s: %s; new filter: %s",
// filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
return;
});
}
return promise.then(function(existingId) {
if (existingId) {
return existingId;
}
// create a new filter
return self.createFilter(filter.getDefinition()
).then(function(createdFilter) {
// debuglog("Created new filter ID %s: %s", createdFilter.filterId,
// JSON.stringify(createdFilter.getDefinition()));
self.store.setFilterIdByName(filterName, createdFilter.filterId);
return createdFilter.filterId;
});
});
};
/** /**
* Gets a bearer token from the Home Server that the user can * Gets a bearer token from the Home Server that the user can
* present to a third party in order to prove their ownership * present to a third party in order to prove their ownership

View File

@@ -522,21 +522,23 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
* store an InboundGroupSession in the session store * store an InboundGroupSession in the session store
* *
* @param {string} roomId * @param {string} roomId
* @param {string} senderKey * @param {string} senderCurve25519Key
* @param {string} sessionId * @param {string} sessionId
* @param {Olm.InboundGroupSession} session * @param {Olm.InboundGroupSession} session
* @param {object} keysClaimed Other keys the sender claims.
* @private * @private
*/ */
OlmDevice.prototype._saveInboundGroupSession = function( OlmDevice.prototype._saveInboundGroupSession = function(
roomId, senderKey, sessionId, session roomId, senderCurve25519Key, sessionId, session, keysClaimed
) { ) {
var r = { var r = {
room_id: roomId, room_id: roomId,
session: session.pickle(this._pickleKey), session: session.pickle(this._pickleKey),
keysClaimed: keysClaimed,
}; };
this._sessionStore.storeEndToEndInboundGroupSession( this._sessionStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, JSON.stringify(r) senderCurve25519Key, sessionId, JSON.stringify(r)
); );
}; };
@@ -547,7 +549,9 @@ OlmDevice.prototype._saveInboundGroupSession = function(
* @param {string} senderKey * @param {string} senderKey
* @param {string} sessionId * @param {string} sessionId
* @param {function} func * @param {function} func
* @return {object} result of func * @return {object} Object with two keys "result": result of func, "exists"
* whether the session exists. if the session doesn't exist then the function
* isn't called and the "result" is undefined.
* @private * @private
*/ */
OlmDevice.prototype._getInboundGroupSession = function( OlmDevice.prototype._getInboundGroupSession = function(
@@ -558,7 +562,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
); );
if (r === null) { if (r === null) {
throw new Error("Unknown inbound group session id"); return {sessionExists: false};
} }
r = JSON.parse(r); r = JSON.parse(r);
@@ -575,7 +579,12 @@ OlmDevice.prototype._getInboundGroupSession = function(
var session = new Olm.InboundGroupSession(); var session = new Olm.InboundGroupSession();
try { try {
session.unpickle(this._pickleKey, r.session); session.unpickle(this._pickleKey, r.session);
return func(session); return {
sessionExists: true,
result: func(session),
keysProved: {curve25519: senderKey},
keysClaimed: r.keysClaimed || {},
};
} finally { } finally {
session.free(); session.free();
} }
@@ -616,7 +625,7 @@ OlmDevice.prototype.addInboundGroupSession = function(
* @param {string} sessionId session identifier * @param {string} sessionId session identifier
* @param {string} body base64-encoded body of the encrypted message * @param {string} body base64-encoded body of the encrypted message
* *
* @return {string} plaintext * @return {object} {result: "plaintext"|undefined, sessionExists: Boolean}
*/ */
OlmDevice.prototype.decryptGroupMessage = function( OlmDevice.prototype.decryptGroupMessage = function(
roomId, senderKey, sessionId, body roomId, senderKey, sessionId, body

View File

@@ -272,6 +272,9 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
sender_key: self._olmDevice.deviceCurve25519Key, sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext, ciphertext: ciphertext,
session_id: session_id, session_id: session_id,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
device_id: self._deviceId,
}; };
return encryptedContent; return encryptedContent;
@@ -356,7 +359,8 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
* *
* @param {object} event raw event * @param {object} event raw event
* *
* @return {object} decrypted payload (with properties 'type', 'content') * @return {object} object with 'result' key with decrypted payload (with
* properties 'type', 'content') and a 'sessionExists' key.
* *
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a * @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event * problem decrypting the event
@@ -377,7 +381,12 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
var res = this._olmDevice.decryptGroupMessage( var res = this._olmDevice.decryptGroupMessage(
event.room_id, content.sender_key, content.session_id, content.ciphertext event.room_id, content.sender_key, content.session_id, content.ciphertext
); );
return JSON.parse(res); if (res.sessionExists) {
res.result = JSON.parse(res.result);
return res;
} else {
return {sessionExists: false};
}
} catch (e) { } catch (e) {
throw new base.DecryptionError(e); throw new base.DecryptionError(e);
} }
@@ -402,7 +411,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
this._olmDevice.addInboundGroupSession( this._olmDevice.addInboundGroupSession(
content.room_id, event.getSenderKey(), content.session_id, content.room_id, event.getSenderKey(), content.session_id,
content.session_key content.session_key, event.getKeysClaimed()
); );
}; };

View File

@@ -119,6 +119,17 @@ OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
room_id: room.roomId, room_id: room.roomId,
type: eventType, type: eventType,
content: content, content: content,
// Include the ED25519 key so that the recipient knows what
// device this message came from.
// We don't need to include the curve25519 key since the
// recipient will already know this from the olm headers.
// When combined with the device keys retrieved from the
// homeserver signed by the ed25519 key this proves that
// the curve25519 key and the ed25519 key are owned by
// the same device.
keys: {
"ed25519": self._olmDevice.deviceEd25519Key
},
} }
); );
}); });
@@ -142,7 +153,9 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
* *
* @param {object} event raw event * @param {object} event raw event
* *
* @return {object} decrypted payload (with properties 'type', 'content') * @return {object} result object with result property with the decrypted
* payload (with properties 'type', 'content'), and a "sessionExists" key
* always set to true.
* *
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a * @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event * problem decrypting the event
@@ -198,7 +211,13 @@ OlmDecryption.prototype.decryptEvent = function(event) {
// TODO: Check the sender user id matches the sender key. // TODO: Check the sender user id matches the sender key.
// TODO: check the room_id and fingerprint // TODO: check the room_id and fingerprint
if (payloadString !== null) { if (payloadString !== null) {
return JSON.parse(payloadString); var payload = JSON.parse(payloadString);
return {
result: payload,
sessionExists: true,
keysProved: {curve25519: deviceKey},
keysClaimed: payload.keys || {}
};
} else { } else {
throw new base.DecryptionError("Bad Encrypted Message"); throw new base.DecryptionError("Bad Encrypted Message");
} }

View File

@@ -85,36 +85,53 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
); );
_registerEventHandlers(this, eventEmitter); _registerEventHandlers(this, eventEmitter);
this._lastNewDeviceMessageTsByUserDeviceRoom = {};
} }
function _registerEventHandlers(crypto, eventEmitter) { function _registerEventHandlers(crypto, eventEmitter) {
eventEmitter.on("sync", function(syncState, oldState, data) { eventEmitter.on("sync", function(syncState, oldState, data) {
try {
if (syncState == "PREPARED") { if (syncState == "PREPARED") {
// XXX ugh. we're assuming the eventEmitter is a MatrixClient. // XXX ugh. we're assuming the eventEmitter is a MatrixClient.
// how can we avoid doing so? // how can we avoid doing so?
var rooms = eventEmitter.getRooms(); var rooms = eventEmitter.getRooms();
crypto._onInitialSyncCompleted(rooms); crypto._onInitialSyncCompleted(rooms);
} }
} catch (e) {
console.error("Error handling sync", e);
}
}); });
eventEmitter.on( eventEmitter.on("RoomMember.membership", function(event, member, oldMembership) {
"RoomMember.membership", try {
crypto._onRoomMembership.bind(crypto) crypto._onRoomMembership(event, member, oldMembership);
); } catch (e) {
console.error("Error handling membership change:", e);
}
});
eventEmitter.on("toDeviceEvent", function(event) { eventEmitter.on("toDeviceEvent", function(event) {
try {
if (event.getType() == "m.room_key") { if (event.getType() == "m.room_key") {
crypto._onRoomKeyEvent(event); crypto._onRoomKeyEvent(event);
} else if (event.getType() == "m.new_device") { } else if (event.getType() == "m.new_device") {
crypto._onNewDeviceEvent(event); crypto._onNewDeviceEvent(event);
} }
} catch (e) {
console.error("Error handling toDeviceEvent:", e);
}
}); });
eventEmitter.on("event", function(event) { eventEmitter.on("event", function(event) {
try {
if (!event.isState() || event.getType() != "m.room.encryption") { if (!event.isState() || event.getType() != "m.room.encryption") {
return; return;
} }
crypto._onCryptoEvent(event); crypto._onCryptoEvent(event);
} catch (e) {
console.error("Error handling crypto event:", e);
}
}); });
} }
@@ -812,7 +829,66 @@ Crypto.prototype.decryptEvent = function(event) {
var alg = new AlgClass({ var alg = new AlgClass({
olmDevice: this._olmDevice, olmDevice: this._olmDevice,
}); });
return alg.decryptEvent(event); var r = alg.decryptEvent(event);
var payload = r.result;
payload.keysClaimed = r.keysClaimed;
payload.keysProved = r.keysProved;
if (r.sessionExists) {
return payload;
} else {
// We've got a message for a session we don't have.
// Maybe the sender forgot to tell us about the session.
// Remind the sender that we exist so that they might
// tell us about the sender.
if (event.getRoomId !== undefined && event.getSender !== undefined) {
this._sendPingToDevice(
event.getSender(), event.content.device, event.getRoomId
);
}
throw new algorithms.DecryptionError("Unknown inbound session id");
}
};
/**
* Send a "m.new_device" message to remind it that we exist and are a member
* of a room.
*
* This is rate limited to send a message at most once an hour per desination.
*
* @param {string} userId The ID of the user to ping.
* @param {string} deviceId The ID of the device to ping.
* @param {string} roomId The ID of the room we want to remind them about.
*/
Crypto.prototype._sendPingToDevice = function(userId, deviceId, roomId) {
if (deviceId === undefined) {
deviceId = "*";
}
var lastMessageTsMap = this._lastNewDeviceMessageTsByUserDeviceRoom;
var lastTsByDevice = lastMessageTsMap[userId] || {};
var lastTsByRoom = lastTsByDevice[deviceId] || {};
var lastTs = lastTsByRoom[roomId];
var timeNowMs = Date.now();
var oneHourMs = 1000 * 60 * 60;
if (lastTs === undefined || lastTs + oneHourMs < timeNowMs) {
var content = {
userId: {
deviceId: {
device_id: this._deviceId,
rooms: [roomId],
}
}
};
lastTsByRoom[roomId] = timeNowMs;
this._baseApis.sendToDevice(
"m.new_device", // OH HAI!
content
).done(function() {});
}
}; };
/** /**

141
lib/filter-component.js Normal file
View File

@@ -0,0 +1,141 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module filter-component
*/
/**
* Checks if a value matches a given field value, which may be a * terminated
* wildcard pattern.
* @param {String} actual_value The value to be compared
* @param {String} filter_value The filter pattern to be compared
* @return {bool} true if the actual_value matches the filter_value
*/
function _matches_wildcard(actual_value, filter_value) {
if (filter_value.endsWith("*")) {
var type_prefix = filter_value.slice(0, -1);
return actual_value.substr(0, type_prefix.length) === type_prefix;
}
else {
return actual_value === filter_value;
}
}
/**
* FilterComponent is a section of a Filter definition which defines the
* types, rooms, senders filters etc to be applied to a particular type of resource.
* This is all ported over from synapse's Filter object.
*
* N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as
* 'Filters' are referred to as 'FilterCollections'.
*
* @constructor
* @param {Object} the definition of this filter JSON, e.g. { 'contains_url': true }
*/
function FilterComponent(filter_json) {
this.filter_json = filter_json;
this.types = filter_json.types || null;
this.not_types = filter_json.not_types || [];
this.rooms = filter_json.rooms || null;
this.not_rooms = filter_json.not_rooms || [];
this.senders = filter_json.senders || null;
this.not_senders = filter_json.not_senders || [];
this.contains_url = filter_json.contains_url || null;
}
/**
* Checks with the filter component matches the given event
* @param {MatrixEvent} event event to be checked against the filter
* @return {bool} true if the event matches the filter
*/
FilterComponent.prototype.check = function(event) {
return this._checkFields(
event.getRoomId(),
event.getSender(),
event.getType(),
event.getContent() ? event.getContent().url !== undefined : false
);
};
/**
* Checks whether the filter component matches the given event fields.
* @param {String} room_id the room_id for the event being checked
* @param {String} sender the sender of the event being checked
* @param {String} event_type the type of the event being checked
* @param {String} contains_url whether the event contains a content.url field
* @return {bool} true if the event fields match the filter
*/
FilterComponent.prototype._checkFields =
function(room_id, sender, event_type, contains_url)
{
var literal_keys = {
"rooms": function(v) { return room_id === v; },
"senders": function(v) { return sender === v; },
"types": function(v) { return _matches_wildcard(event_type, v); },
};
var self = this;
Object.keys(literal_keys).forEach(function(name) {
var match_func = literal_keys[name];
var not_name = "not_" + name;
var disallowed_values = self[not_name];
if (disallowed_values.map(match_func)) {
return false;
}
var allowed_values = self[name];
if (allowed_values) {
if (!allowed_values.map(match_func)) {
return false;
}
}
});
var contains_url_filter = this.filter_json.contains_url;
if (contains_url_filter !== undefined) {
if (contains_url_filter !== contains_url) {
return false;
}
}
return true;
};
/**
* Filters a list of events down to those which match this filter component
* @param {MatrixEvent[]} events Events to be checked againt the filter component
* @return {MatrixEvent[]} events which matched the filter component
*/
FilterComponent.prototype.filter = function(events) {
return events.filter(this.check, this);
};
/**
* Returns the limit field for a given filter component, providing a default of
* 10 if none is otherwise specified. Cargo-culted from Synapse.
* @return {Number} the limit for this filter component.
*/
FilterComponent.prototype.limit = function() {
return this.filter_json.limit !== undefined ? this.filter_json.limit : 10;
};
/** The FilterComponent class */
module.exports = FilterComponent;

View File

@@ -18,6 +18,8 @@ limitations under the License.
* @module filter * @module filter
*/ */
var FilterComponent = require("./filter-component");
/** /**
* @param {Object} obj * @param {Object} obj
* @param {string} keyNesting * @param {string} keyNesting
@@ -49,6 +51,14 @@ function Filter(userId, filterId) {
this.definition = {}; this.definition = {};
} }
/**
* Get the ID of this filter on your homeserver (if known)
* @return {?Number} The filter ID
*/
Filter.prototype.getFilterId = function() {
return this.filterId;
};
/** /**
* Get the JSON body of the filter. * Get the JSON body of the filter.
* @return {Object} The filter definition * @return {Object} The filter definition
@@ -63,6 +73,88 @@ Filter.prototype.getDefinition = function() {
*/ */
Filter.prototype.setDefinition = function(definition) { Filter.prototype.setDefinition = function(definition) {
this.definition = definition; this.definition = definition;
// This is all ported from synapse's FilterCollection()
// definitions look something like:
// {
// "room": {
// "rooms": ["!abcde:example.com"],
// "not_rooms": ["!123456:example.com"],
// "state": {
// "types": ["m.room.*"],
// "not_rooms": ["!726s6s6q:example.com"],
// },
// "timeline": {
// "limit": 10,
// "types": ["m.room.message"],
// "not_rooms": ["!726s6s6q:example.com"],
// "not_senders": ["@spam:example.com"]
// "contains_url": true
// },
// "ephemeral": {
// "types": ["m.receipt", "m.typing"],
// "not_rooms": ["!726s6s6q:example.com"],
// "not_senders": ["@spam:example.com"]
// }
// },
// "presence": {
// "types": ["m.presence"],
// "not_senders": ["@alice:example.com"]
// },
// "event_format": "client",
// "event_fields": ["type", "content", "sender"]
// }
var room_filter_json = definition.room;
// consider the top level rooms/not_rooms filter
var room_filter_fields = {};
if (room_filter_json) {
if (room_filter_json.rooms) {
room_filter_fields.rooms = room_filter_json.rooms;
}
if (room_filter_json.rooms) {
room_filter_fields.not_rooms = room_filter_json.not_rooms;
}
this._include_leave = room_filter_json.include_leave || false;
}
this._room_filter = new FilterComponent(room_filter_fields);
this._room_timeline_filter = new FilterComponent(
room_filter_json ? (room_filter_json.timeline || {}) : {}
);
// don't bother porting this from synapse yet:
// this._room_state_filter =
// new FilterComponent(room_filter_json.state || {});
// this._room_ephemeral_filter =
// new FilterComponent(room_filter_json.ephemeral || {});
// this._room_account_data_filter =
// new FilterComponent(room_filter_json.account_data || {});
// this._presence_filter =
// new FilterComponent(definition.presence || {});
// this._account_data_filter =
// new FilterComponent(definition.account_data || {});
};
/**
* Get the room.timeline filter component of the filter
* @return {FilterComponent} room timeline filter component
*/
Filter.prototype.getRoomTimelineFilterComponent = function() {
return this._room_timeline_filter;
};
/**
* Filter the list of events based on whether they are allowed in a timeline
* based on this filter
* @param {MatrixEvent[]} events the list of events being filtered
* @return {MatrixEvent[]} the list of events which match the filter
*/
Filter.prototype.filterRoomTimeline = function(events) {
return this._room_timeline_filter.filter(this._room_filter.filter(events));
}; };
/** /**

View File

@@ -34,6 +34,8 @@ module.exports.MatrixClient = require("./client").MatrixClient;
module.exports.Room = require("./models/room"); module.exports.Room = require("./models/room");
/** The {@link module:models/event-timeline~EventTimeline} class. */ /** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline"); module.exports.EventTimeline = require("./models/event-timeline");
/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */
module.exports.EventTimelineSet = require("./models/event-timeline-set");
/** The {@link module:models/room-member|RoomMember} class. */ /** The {@link module:models/room-member|RoomMember} class. */
module.exports.RoomMember = require("./models/room-member"); module.exports.RoomMember = require("./models/room-member");
/** The {@link module:models/room-state~RoomState|RoomState} class. */ /** The {@link module:models/room-state~RoomState|RoomState} class. */

View File

@@ -0,0 +1,654 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/event-timeline-set
*/
var EventEmitter = require("events").EventEmitter;
var utils = require("../utils");
var EventTimeline = require("./event-timeline");
// var DEBUG = false;
var DEBUG = true;
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function() {};
}
/**
* Construct a set of EventTimeline objects, typically on behalf of a given
* room. A room may have multiple EventTimelineSets for different levels
* of filtering. The global notification list is also an EventTimelineSet, but
* lacks a room.
*
* <p>This is an ordered sequence of timelines, which may or may not
* be continuous. Each timeline lists a series of events, as well as tracking
* the room state at the start and the end of the timeline (if appropriate).
* It also tracks forward and backward pagination tokens, as well as containing
* links to the next timeline in the sequence.
*
* <p>There is one special timeline - the 'live' timeline, which represents the
* timeline to which events are being added in real-time as they are received
* from the /sync API. Note that you should not retain references to this
* timeline - even if it is the current timeline right now, it may not remain
* so if the server gives us a timeline gap in /sync.
*
* <p>In order that we can find events from their ids later, we also maintain a
* map from event_id to timeline and index.
*
* @constructor
* @param {?Room} room the optional room for this timelineSet
* @param {Object} opts hash of options inherited from Room.
* opts.timelineSupport gives whether timeline support is enabled
* opts.filter is the filter object, if any, for this timelineSet.
*/
function EventTimelineSet(room, opts) {
this.room = room;
this._timelineSupport = Boolean(opts.timelineSupport);
this._liveTimeline = new EventTimeline(this);
// just a list - *not* ordered.
this._timelines = [this._liveTimeline];
this._eventIdToTimeline = {};
this._filter = opts.filter || null;
}
utils.inherits(EventTimelineSet, EventEmitter);
/**
* Get the filter object this timeline set is filtered on, if any
* @return {?Filter} the optional filter for this timelineSet
*/
EventTimelineSet.prototype.getFilter = function() {
return this._filter;
};
/**
* Set the filter object this timeline set is filtered on
* (passed to the server when paginating via /messages).
* @param {Filter} filter the filter for this timelineSet
*/
EventTimelineSet.prototype.setFilter = function(filter) {
this._filter = filter;
};
/**
* Get the list of pending sent events for this timelineSet's room, filtered
* by the timelineSet's filter if appropriate.
*
* @return {module:models/event.MatrixEvent[]} A list of the sent events
* waiting for remote echo.
*
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
EventTimelineSet.prototype.getPendingEvents = function() {
if (!this.room) {
return [];
}
if (this._filter) {
return this._filter.filterRoomTimeline(this.room.getPendingEvents());
}
else {
return this.room.getPendingEvents();
}
};
/**
* Get the live timeline for this room.
*
* @return {module:models/event-timeline~EventTimeline} live timeline
*/
EventTimelineSet.prototype.getLiveTimeline = function() {
return this._liveTimeline;
};
/**
* Return the timeline (if any) this event is in.
* @param {String} eventId the eventId being sought
* @return {module:models/event-timeline~EventTimeline} timeline
*/
EventTimelineSet.prototype.eventIdToTimeline = function(eventId) {
return this._eventIdToTimeline[eventId];
};
/**
* Track a new event as if it were in the same timeline as an old event,
* replacing it.
* @param {String} oldEventId event ID of the original event
* @param {String} newEventId event ID of the replacement event
*/
EventTimelineSet.prototype.replaceEventId = function(oldEventId, newEventId) {
var existingTimeline = this._eventIdToTimeline[oldEventId];
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
}
};
/**
* Reset the live timeline, and start a new one.
*
* <p>This is used when /sync returns a 'limited' timeline.
*
* @param {string=} backPaginationToken token for back-paginating the new timeline
* @param {?bool} flush Whether to flush the non-live timelines too.
*
* @fires module:client~MatrixClient#event:"Room.timelineReset"
*/
EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flush) {
var newTimeline;
if (!this._timelineSupport || flush) {
// if timeline support is disabled, forget about the old timelines
newTimeline = new EventTimeline(this);
this._timelines = [newTimeline];
this._eventIdToTimeline = {};
} else {
newTimeline = this.addTimeline();
}
// initialise the state in the new timeline from our last known state
var evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events;
var events = [];
for (var evtype in evMap) {
if (!evMap.hasOwnProperty(evtype)) { continue; }
for (var stateKey in evMap[evtype]) {
if (!evMap[evtype].hasOwnProperty(stateKey)) { continue; }
events.push(evMap[evtype][stateKey]);
}
}
newTimeline.initialiseState(events);
// make sure we set the pagination token before firing timelineReset,
// otherwise clients which start back-paginating will fail, and then get
// stuck without realising that they *can* back-paginate.
newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS);
this._liveTimeline = newTimeline;
this.emit("Room.timelineReset", this.room, this);
};
/**
* Get the timeline which contains the given event, if any
*
* @param {string} eventId event ID to look for
* @return {?module:models/event-timeline~EventTimeline} timeline containing
* the given event, or null if unknown
*/
EventTimelineSet.prototype.getTimelineForEvent = function(eventId) {
var res = this._eventIdToTimeline[eventId];
return (res === undefined) ? null : res;
};
/**
* Get an event which is stored in our timelines
*
* @param {string} eventId event ID to look for
* @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown
*/
EventTimelineSet.prototype.findEventById = function(eventId) {
var tl = this.getTimelineForEvent(eventId);
if (!tl) {
return undefined;
}
return utils.findElement(tl.getEvents(),
function(ev) { return ev.getId() == eventId; });
};
/**
* Add a new timeline to this timeline list
*
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
*/
EventTimelineSet.prototype.addTimeline = function() {
if (!this._timelineSupport) {
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
" parameter to true when creating MatrixClient to enable" +
" it.");
}
var timeline = new EventTimeline(this);
this._timelines.push(timeline);
return timeline;
};
/**
* Add events to a timeline
*
* <p>Will fire "Room.timeline" for each event added.
*
* @param {MatrixEvent[]} events A list of events to add.
*
* @param {boolean} toStartOfTimeline True to add these events to the start
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
* event will be the <b>last</b> element of 'events'.
*
* @param {module:models/event-timeline~EventTimeline} timeline timeline to
* add events to.
*
* @param {string=} paginationToken token for the next batch of events
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*
*/
EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
timeline, paginationToken) {
if (!timeline) {
throw new Error(
"'timeline' not specified for EventTimelineSet.addEventsToTimeline"
);
}
if (!toStartOfTimeline && timeline == this._liveTimeline) {
throw new Error(
"EventTimelineSet.addEventsToTimeline cannot be used for adding events to " +
"the live timeline - use Room.addLiveEvents instead"
);
}
if (this._filter) {
events = this._filter.filterRoomTimeline(events);
if (!events.length) {
return;
}
}
var direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
EventTimeline.FORWARDS;
var inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS :
EventTimeline.BACKWARDS;
// Adding events to timelines can be quite complicated. The following
// illustrates some of the corner-cases.
//
// Let's say we start by knowing about four timelines. timeline3 and
// timeline4 are neighbours:
//
// timeline1 timeline2 timeline3 timeline4
// [M] [P] [S] <------> [T]
//
// Now we paginate timeline1, and get the following events from the server:
// [M, N, P, R, S, T, U].
//
// 1. First, we ignore event M, since we already know about it.
//
// 2. Next, we append N to timeline 1.
//
// 3. Next, we don't add event P, since we already know about it,
// but we do link together the timelines. We now have:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P] [S] <------> [T]
//
// 4. Now we add event R to timeline2:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P, R] [S] <------> [T]
//
// Note that we have switched the timeline we are working on from
// timeline1 to timeline2.
//
// 5. We ignore event S, but again join the timelines:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P, R] <---> [S] <------> [T]
//
// 6. We ignore event T, and the timelines are already joined, so there
// is nothing to do.
//
// 7. Finally, we add event U to timeline4:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P, R] <---> [S] <------> [T, U]
//
// The important thing to note in the above is what happened when we
// already knew about a given event:
//
// - if it was appropriate, we joined up the timelines (steps 3, 5).
// - in any case, we started adding further events to the timeline which
// contained the event we knew about (steps 3, 5, 6).
//
//
// So much for adding events to the timeline. But what do we want to do
// with the pagination token?
//
// In the case above, we will be given a pagination token which tells us how to
// get events beyond 'U' - in this case, it makes sense to store this
// against timeline4. But what if timeline4 already had 'U' and beyond? in
// that case, our best bet is to throw away the pagination token we were
// given and stick with whatever token timeline4 had previously. In short,
// we want to only store the pagination token if the last event we receive
// is one we didn't previously know about.
//
// We make an exception for this if it turns out that we already knew about
// *all* of the events, and we weren't able to join up any timelines. When
// that happens, it means our existing pagination token is faulty, since it
// is only telling us what we already know. Rather than repeatedly
// paginating with the same token, we might as well use the new pagination
// token in the hope that we eventually work our way out of the mess.
var didUpdate = false;
var lastEventWasNew = false;
for (var i = 0; i < events.length; i++) {
var event = events[i];
var eventId = event.getId();
var existingTimeline = this._eventIdToTimeline[eventId];
if (!existingTimeline) {
// we don't know about this event yet. Just add it to the timeline.
this.addEventToTimeline(event, timeline, toStartOfTimeline);
lastEventWasNew = true;
didUpdate = true;
continue;
}
lastEventWasNew = false;
if (existingTimeline == timeline) {
debuglog("Event " + eventId + " already in timeline " + timeline);
continue;
}
var neighbour = timeline.getNeighbouringTimeline(direction);
if (neighbour) {
// this timeline already has a neighbour in the relevant direction;
// let's assume the timelines are already correctly linked up, and
// skip over to it.
//
// there's probably some edge-case here where we end up with an
// event which is in a timeline a way down the chain, and there is
// a break in the chain somewhere. But I can't really imagine how
// that would happen, so I'm going to ignore it for now.
//
if (existingTimeline == neighbour) {
debuglog("Event " + eventId + " in neighbouring timeline - " +
"switching to " + existingTimeline);
} else {
debuglog("Event " + eventId + " already in a different " +
"timeline " + existingTimeline);
}
timeline = existingTimeline;
continue;
}
// time to join the timelines.
console.info("Already have timeline for " + eventId +
" - joining timeline " + timeline + " to " +
existingTimeline);
timeline.setNeighbouringTimeline(existingTimeline, direction);
existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
timeline = existingTimeline;
didUpdate = true;
}
// see above - if the last event was new to us, or if we didn't find any
// new information, we update the pagination token for whatever
// timeline we ended up on.
if (lastEventWasNew || !didUpdate) {
timeline.setPaginationToken(paginationToken, direction);
}
};
/**
* Add an event to the end of this live timeline.
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
*/
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
if (this._filter) {
var events = this._filter.filterRoomTimeline([event]);
if (!events.length) {
return;
}
}
var timeline = this._eventIdToTimeline[event.getId()];
if (timeline) {
if (duplicateStrategy === "replace") {
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " +
event.getId());
var tlEvents = timeline.getEvents();
for (var j = 0; j < tlEvents.length; j++) {
if (tlEvents[j].getId() === event.getId()) {
// still need to set the right metadata on this event
EventTimeline.setEventMetadata(
event,
timeline.getState(EventTimeline.FORWARDS),
false
);
if (!tlEvents[j].encryptedType) {
tlEvents[j] = event;
}
// XXX: we need to fire an event when this happens.
break;
}
}
} else {
debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " +
event.getId());
}
return;
}
this.addEventToTimeline(event, this._liveTimeline, false);
};
/**
* Add event to the given timeline, and emit Room.timeline. Assumes
* we have already checked we don't know about this event.
*
* Will fire "Room.timeline" for each event added.
*
* @param {MatrixEvent} event
* @param {EventTimeline} timeline
* @param {boolean} toStartOfTimeline
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*/
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
toStartOfTimeline) {
var eventId = event.getId();
timeline.addEvent(event, toStartOfTimeline);
this._eventIdToTimeline[eventId] = timeline;
var data = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
};
this.emit("Room.timeline", event, this.room,
Boolean(toStartOfTimeline), false, data);
};
/**
* Replaces event with ID oldEventId with one with newEventId, if oldEventId is
* recognised. Otherwise, add to the live timeline. Used to handle remote echos.
*
* @param {MatrixEvent} localEvent the new event to be added to the timeline
* @param {String} oldEventId the ID of the original event
* @param {boolean} newEventId the ID of the replacement event
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*/
EventTimelineSet.prototype.handleRemoteEcho = function(localEvent, oldEventId,
newEventId) {
// XXX: why don't we infer newEventId from localEvent?
var existingTimeline = this._eventIdToTimeline[oldEventId];
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
} else {
if (this._filter) {
if (this._filter.filterRoomTimeline([localEvent]).length) {
this.addEventToTimeline(localEvent, this._liveTimeline, false);
}
}
else {
this.addEventToTimeline(localEvent, this._liveTimeline, false);
}
}
};
/**
* Removes a single event from this room.
*
* @param {String} eventId The id of the event to remove
*
* @return {?MatrixEvent} the removed event, or null if the event was not found
* in this room.
*/
EventTimelineSet.prototype.removeEvent = function(eventId) {
var timeline = this._eventIdToTimeline[eventId];
if (!timeline) {
return null;
}
var removed = timeline.removeEvent(eventId);
if (removed) {
delete this._eventIdToTimeline[eventId];
var data = {
timeline: timeline,
};
this.emit("Room.timeline", removed, this.room, undefined, true, data);
}
return removed;
};
/**
* Determine where two events appear in the timeline relative to one another
*
* @param {string} eventId1 The id of the first event
* @param {string} eventId2 The id of the second event
* @return {?number} a number less than zero if eventId1 precedes eventId2, and
* greater than zero if eventId1 succeeds eventId2. zero if they are the
* same event; null if we can't tell (either because we don't know about one
* of the events, or because they are in separate timelines which don't join
* up).
*/
EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
if (eventId1 == eventId2) {
// optimise this case
return 0;
}
var timeline1 = this._eventIdToTimeline[eventId1];
var timeline2 = this._eventIdToTimeline[eventId2];
if (timeline1 === undefined) {
return null;
}
if (timeline2 === undefined) {
return null;
}
if (timeline1 === timeline2) {
// both events are in the same timeline - figure out their
// relative indices
var idx1, idx2;
var events = timeline1.getEvents();
for (var idx = 0; idx < events.length &&
(idx1 === undefined || idx2 === undefined); idx++) {
var evId = events[idx].getId();
if (evId == eventId1) {
idx1 = idx;
}
if (evId == eventId2) {
idx2 = idx;
}
}
return idx1 - idx2;
}
// the events are in different timelines. Iterate through the
// linkedlist to see which comes first.
// first work forwards from timeline1
var tl = timeline1;
while (tl) {
if (tl === timeline2) {
// timeline1 is before timeline2
return -1;
}
tl = tl.getNeighbouringTimeline(EventTimeline.FORWARDS);
}
// now try backwards from timeline1
tl = timeline1;
while (tl) {
if (tl === timeline2) {
// timeline2 is before timeline1
return 1;
}
tl = tl.getNeighbouringTimeline(EventTimeline.BACKWARDS);
}
// the timelines are not contiguous.
return null;
};
/**
* The EventTimelineSet class.
*/
module.exports = EventTimelineSet;
/**
* Fires whenever the timeline in a room is updated.
* @event module:client~MatrixClient#"Room.timeline"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {?Room} room The room, if any, whose timeline was updated.
* @param {boolean} toStartOfTimeline True if this event was added to the start
* @param {boolean} removed True if this event has just been removed from the timeline
* (beginning; oldest) of the timeline e.g. due to pagination.
*
* @param {object} data more data about the event
*
* @param {module:event-timeline.EventTimeline} data.timeline the timeline the
* event was added to/removed from
*
* @param {boolean} data.liveEvent true if the event was a real-time event
* added to the end of the live timeline
*
* @example
* matrixClient.on("Room.timeline",
* function(event, room, toStartOfTimeline, removed, data) {
* if (!toStartOfTimeline && data.liveEvent) {
* var messageToAppend = room.timeline.[room.timeline.length - 1];
* }
* });
*/
/**
* Fires whenever the live timeline in a room is reset.
*
* When we get a 'limited' sync (for example, after a network outage), we reset
* the live timeline to be empty before adding the recent events to the new
* timeline. This event is fired after the timeline is reset, and before the
* new events are added.
*
* @event module:client~MatrixClient#"Room.timelineReset"
* @param {Room} room The room whose live timeline was reset, if any
* @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset
*/

View File

@@ -25,16 +25,17 @@ var MatrixEvent = require("./event").MatrixEvent;
* <p>Once a timeline joins up with its neighbour, they are linked together into a * <p>Once a timeline joins up with its neighbour, they are linked together into a
* doubly-linked list. * doubly-linked list.
* *
* @param {string} roomId the ID of the room where this timeline came from * @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of
* @constructor * @constructor
*/ */
function EventTimeline(roomId) { function EventTimeline(eventTimelineSet) {
this._roomId = roomId; this._eventTimelineSet = eventTimelineSet;
this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null;
this._events = []; this._events = [];
this._baseIndex = 0; this._baseIndex = 0;
this._startState = new RoomState(roomId); this._startState = new RoomState(this._roomId);
this._startState.paginationToken = null; this._startState.paginationToken = null;
this._endState = new RoomState(roomId); this._endState = new RoomState(this._roomId);
this._endState.paginationToken = null; this._endState.paginationToken = null;
this._prevTimeline = null; this._prevTimeline = null;
@@ -43,7 +44,7 @@ function EventTimeline(roomId) {
// this is used by client.js // this is used by client.js
this._paginationRequests = {'b': null, 'f': null}; this._paginationRequests = {'b': null, 'f': null};
this._name = roomId + ":" + new Date().toISOString(); this._name = this._roomId + ":" + new Date().toISOString();
} }
/** /**
@@ -91,6 +92,22 @@ EventTimeline.prototype.getRoomId = function() {
return this._roomId; return this._roomId;
}; };
/**
* Get the filter for this timeline's timelineSet (if any)
* @return {Filter} filter
*/
EventTimeline.prototype.getFilter = function() {
return this._eventTimelineSet.getFilter();
};
/**
* Get the timelineSet for this timeline
* @return {EventTimelineSet} timelineSet
*/
EventTimeline.prototype.getTimelineSet = function() {
return this._eventTimelineSet;
};
/** /**
* Get the base index. * Get the base index.
* *
@@ -217,7 +234,12 @@ EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction)
EventTimeline.prototype.addEvent = function(event, atStart) { EventTimeline.prototype.addEvent = function(event, atStart) {
var stateContext = atStart ? this._startState : this._endState; var stateContext = atStart ? this._startState : this._endState;
setEventMetadata(event, stateContext, atStart); // only call setEventMetadata on the unfiltered timelineSets
var timelineSet = this.getTimelineSet();
if (timelineSet.room &&
timelineSet.room.getUnfilteredTimelineSet() === timelineSet)
{
EventTimeline.setEventMetadata(event, stateContext, atStart);
// modify state // modify state
if (event.isState()) { if (event.isState()) {
@@ -233,7 +255,8 @@ EventTimeline.prototype.addEvent = function(event, atStart) {
// member event, whereas we want to set the .sender value for the ACTUAL // member event, whereas we want to set the .sender value for the ACTUAL
// member event itself. // member event itself.
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) { if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
setEventMetadata(event, stateContext, atStart); EventTimeline.setEventMetadata(event, stateContext, atStart);
}
} }
} }
@@ -251,7 +274,14 @@ EventTimeline.prototype.addEvent = function(event, atStart) {
} }
}; };
function setEventMetadata(event, stateContext, toStartOfTimeline) { /**
* Static helper method to set sender and target properties
*
* @param {MatrixEvent} event the event whose metadata is to be set
* @param {RoomState} stateContext the room state to be queried
* @param {bool} toStartOfTimeline if true the event's forwardLooking flag is set false
*/
EventTimeline.setEventMetadata = function(event, stateContext, toStartOfTimeline) {
// set sender and target properties // set sender and target properties
event.sender = stateContext.getSentinelMember( event.sender = stateContext.getSentinelMember(
event.getSender() event.getSender()
@@ -270,7 +300,7 @@ function setEventMetadata(event, stateContext, toStartOfTimeline) {
event.forwardLooking = false; event.forwardLooking = false;
} }
} }
} };
/** /**
* Remove an event from the timeline * Remove an event from the timeline

View File

@@ -76,6 +76,7 @@ module.exports.MatrixEvent = function MatrixEvent(event, clearEvent) {
this.forwardLooking = true; this.forwardLooking = true;
this._clearEvent = clearEvent || {}; this._clearEvent = clearEvent || {};
this._pushActions = null;
}; };
module.exports.MatrixEvent.prototype = { module.exports.MatrixEvent.prototype = {
@@ -232,12 +233,28 @@ module.exports.MatrixEvent.prototype = {
return Boolean(this._clearEvent.type); return Boolean(this._clearEvent.type);
}, },
/**
* The curve25519 key that sent this event
* @return {string}
*/
getSenderKey: function() { getSenderKey: function() {
if (!this.isEncrypted()) { return this.getKeysProved().curve25519 || null;
return null; },
}
var c = this.getWireContent(); /**
return c.sender_key; * The keys that must have been owned by the sender of this encrypted event.
* @return {object}
*/
getKeysProved: function() {
return this._clearEvent.keysProved || {};
},
/**
* The additional keys the sender of this encrypted event claims to possess
* @return {object}
*/
getKeysClaimed: function() {
return this._clearEvent.keysClaimed || {};
}, },
getUnsigned: function() { getUnsigned: function() {
@@ -294,6 +311,24 @@ module.exports.MatrixEvent.prototype = {
isRedacted: function() { isRedacted: function() {
return Boolean(this.getUnsigned().redacted_because); return Boolean(this.getUnsigned().redacted_because);
}, },
/**
* Get the push actions, if known, for this event
*
* @return {?Object} push actions
*/
getPushActions: function() {
return this._pushActions;
},
/**
* Set the push actions for this event.
*
* @param {Object} pushActions push actions
*/
setPushActions: function(pushActions) {
this._pushActions = pushActions;
},
}; };

View File

@@ -25,7 +25,8 @@ var RoomMember = require("./room-member");
/** /**
* Construct room state. * Construct room state.
* @constructor * @constructor
* @param {string} roomId Required. The ID of the room which has this state. * @param {?string} roomId Optional. The ID of the room which has this state.
* If none is specified it just tracks paginationTokens, useful for notifTimelineSet
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed * @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
* on the user's ID. * on the user's ID.
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state * @prop {Object.<string, Object.<string, MatrixEvent>>} events The state

View File

@@ -25,17 +25,7 @@ var MatrixEvent = require("./event").MatrixEvent;
var utils = require("../utils"); var utils = require("../utils");
var ContentRepo = require("../content-repo"); var ContentRepo = require("../content-repo");
var EventTimeline = require("./event-timeline"); var EventTimeline = require("./event-timeline");
var EventTimelineSet = require("./event-timeline-set");
// var DEBUG = false;
var DEBUG = true;
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function() {};
}
function synthesizeReceipt(userId, event, receiptType) { function synthesizeReceipt(userId, event, receiptType) {
@@ -159,13 +149,18 @@ function Room(roomId, opts) {
this._notificationCounts = {}; this._notificationCounts = {};
this._liveTimeline = new EventTimeline(this.roomId); // all our per-room timeline sets. the first one is the unfiltered ones;
// the subsequent ones are the filtered ones in no particular order.
this._timelineSets = [new EventTimelineSet(this, opts)];
reEmit(this, this.getUnfilteredTimelineSet(),
["Room.timeline", "Room.timelineReset"]);
this._fixUpLegacyTimelineFields(); this._fixUpLegacyTimelineFields();
// just a list - *not* ordered. // any filtered timeline sets we're maintaining for this room
this._timelines = [this._liveTimeline]; this._filteredTimelineSets = {
this._eventIdToTimeline = {}; // filter_id: timelineSet
this._timelineSupport = Boolean(opts.timelineSupport); };
if (this._opts.pendingEventOrdering == "detached") { if (this._opts.pendingEventOrdering == "detached") {
this._pendingEventList = []; this._pendingEventList = [];
@@ -191,57 +186,29 @@ Room.prototype.getPendingEvents = function() {
return this._pendingEventList; return this._pendingEventList;
}; };
/** /**
* Get the live timeline for this room. * Get the live unfiltered timeline for this room.
* *
* @return {module:models/event-timeline~EventTimeline} live timeline * @return {module:models/event-timeline~EventTimeline} live timeline
*/ */
Room.prototype.getLiveTimeline = function() { Room.prototype.getLiveTimeline = function() {
return this._liveTimeline; return this.getUnfilteredTimelineSet().getLiveTimeline();
}; };
/** /**
* Reset the live timeline, and start a new one. * Reset the live timeline of all timelineSets, and start new ones.
* *
* <p>This is used when /sync returns a 'limited' timeline. * <p>This is used when /sync returns a 'limited' timeline.
* *
* @param {string=} backPaginationToken token for back-paginating the new timeline * @param {string=} backPaginationToken token for back-paginating the new timeline
*
* @fires module:client~MatrixClient#event:"Room.timelineReset"
*/ */
Room.prototype.resetLiveTimeline = function(backPaginationToken) { Room.prototype.resetLiveTimeline = function(backPaginationToken) {
var newTimeline; for (var i = 0; i < this._timelineSets.length; i++) {
this._timelineSets[i].resetLiveTimeline(backPaginationToken);
if (!this._timelineSupport) {
// if timeline support is disabled, forget about the old timelines
newTimeline = new EventTimeline(this.roomId);
this._timelines = [newTimeline];
this._eventIdToTimeline = {};
} else {
newTimeline = this.addTimeline();
} }
// initialise the state in the new timeline from our last known state
var evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events;
var events = [];
for (var evtype in evMap) {
if (!evMap.hasOwnProperty(evtype)) { continue; }
for (var stateKey in evMap[evtype]) {
if (!evMap[evtype].hasOwnProperty(stateKey)) { continue; }
events.push(evMap[evtype][stateKey]);
}
}
newTimeline.initialiseState(events);
// make sure we set the pagination token before firing timelineReset,
// otherwise clients which start back-paginating will fail, and then get
// stuck without realising that they *can* back-paginate.
newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS);
this._liveTimeline = newTimeline;
this._fixUpLegacyTimelineFields(); this._fixUpLegacyTimelineFields();
this.emit("Room.timelineReset", this);
}; };
/** /**
@@ -254,39 +221,59 @@ Room.prototype._fixUpLegacyTimelineFields = function() {
// and this.oldState and this.currentState as references to the // and this.oldState and this.currentState as references to the
// state at the start and end of that timeline. These are more // state at the start and end of that timeline. These are more
// for backwards-compatibility than anything else. // for backwards-compatibility than anything else.
this.timeline = this._liveTimeline.getEvents(); this.timeline = this.getLiveTimeline().getEvents();
this.oldState = this._liveTimeline.getState(EventTimeline.BACKWARDS); this.oldState = this.getLiveTimeline()
this.currentState = this._liveTimeline.getState(EventTimeline.FORWARDS); .getState(EventTimeline.BACKWARDS);
this.currentState = this.getLiveTimeline()
.getState(EventTimeline.FORWARDS);
}; };
/** /**
* Get the timeline which contains the given event, if any * Return the timeline sets for this room.
* @return {EventTimelineSet[]} array of timeline sets for this room
*/
Room.prototype.getTimelineSets = function() {
return this._timelineSets;
};
/**
* Helper to return the main unfiltered timeline set for this room
* @return {EventTimelineSet} room's unfiltered timeline set
*/
Room.prototype.getUnfilteredTimelineSet = function() {
return this._timelineSets[0];
};
/**
* Get the timeline which contains the given event from the unfiltered set, if any
* *
* @param {string} eventId event ID to look for * @param {string} eventId event ID to look for
* @return {?module:models/event-timeline~EventTimeline} timeline containing * @return {?module:models/event-timeline~EventTimeline} timeline containing
* the given event, or null if unknown * the given event, or null if unknown
*/ */
Room.prototype.getTimelineForEvent = function(eventId) { Room.prototype.getTimelineForEvent = function(eventId) {
var res = this._eventIdToTimeline[eventId]; return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId);
return (res === undefined) ? null : res;
}; };
/** /**
* Get an event which is stored in our timelines * Add a new timeline to this room's unfiltered timeline set
*
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
*/
Room.prototype.addTimeline = function() {
return this.getUnfilteredTimelineSet().addTimeline();
};
/**
* Get an event which is stored in our unfiltered timeline set
* *
* @param {string} eventId event ID to look for * @param {string} eventId event ID to look for
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown * @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
*/ */
Room.prototype.findEventById = function(eventId) { Room.prototype.findEventById = function(eventId) {
var tl = this.getTimelineForEvent(eventId); return this.getUnfilteredTimelineSet().findEventById(eventId);
if (!tl) {
return undefined;
}
return utils.findElement(tl.getEvents(),
function(ev) { return ev.getId() == eventId; });
}; };
/** /**
* Get one of the notification counts for this room * Get one of the notification counts for this room
* @param {String} type The type of notification count to get. default: 'total' * @param {String} type The type of notification count to get. default: 'total'
@@ -379,6 +366,33 @@ Room.prototype.getCanonicalAlias = function() {
return null; return null;
}; };
/**
* Add events to a timeline
*
* <p>Will fire "Room.timeline" for each event added.
*
* @param {MatrixEvent[]} events A list of events to add.
*
* @param {boolean} toStartOfTimeline True to add these events to the start
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
* event will be the <b>last</b> element of 'events'.
*
* @param {module:models/event-timeline~EventTimeline} timeline timeline to
* add events to.
*
* @param {string=} paginationToken token for the next batch of events
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*
*/
Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
timeline, paginationToken) {
timeline.getTimelineSet().addEventsToTimeline(
events, toStartOfTimeline,
timeline, paginationToken
);
};
/** /**
* Get a member from the current room state. * Get a member from the current room state.
* @param {string} userId The user ID of the member. * @param {string} userId The user ID of the member.
@@ -438,224 +452,72 @@ Room.prototype.getCanonicalAlias = function() {
}; };
/** /**
* Add a new timeline to this room * Add a timelineSet for this room with the given filter
* * @param {Filter} filter The filter to be applied to this timelineSet
* @return {module:models/event-timeline~EventTimeline} newly-created timeline * @return {EventTimelineSet} The timelineSet
*/ */
Room.prototype.addTimeline = function() { Room.prototype.getOrCreateFilteredTimelineSet = function(filter) {
if (!this._timelineSupport) { if (this._filteredTimelineSets[filter.filterId]) {
throw new Error("timeline support is disabled. Set the 'timelineSupport'" + return this._filteredTimelineSets[filter.filterId];
" parameter to true when creating MatrixClient to enable" + }
" it."); var opts = Object.assign({ filter: filter }, this._opts);
var timelineSet = new EventTimelineSet(this, opts);
reEmit(this, timelineSet, ["Room.timeline", "Room.timelineReset"]);
this._filteredTimelineSets[filter.filterId] = timelineSet;
this._timelineSets.push(timelineSet);
// populate up the new timelineSet with filtered events from our live
// unfiltered timeline.
//
// XXX: This is risky as our timeline
// may have grown huge and so take a long time to filter.
// see https://github.com/vector-im/vector-web/issues/2109
var unfilteredLiveTimeline = this.getLiveTimeline();
unfilteredLiveTimeline.getEvents().forEach(function(event) {
timelineSet.addLiveEvent(event);
});
// find the earliest unfiltered timeline
var timeline = unfilteredLiveTimeline;
while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) {
timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS);
} }
var timeline = new EventTimeline(this.roomId); timelineSet.getLiveTimeline().setPaginationToken(
this._timelines.push(timeline); timeline.getPaginationToken(EventTimeline.BACKWARDS),
return timeline; EventTimeline.BACKWARDS
};
/**
* Add events to a timeline
*
* <p>Will fire "Room.timeline" for each event added.
*
* @param {MatrixEvent[]} events A list of events to add.
*
* @param {boolean} toStartOfTimeline True to add these events to the start
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
* event will be the <b>last</b> element of 'events'.
*
* @param {module:models/event-timeline~EventTimeline} timeline timeline to
* add events to.
*
* @param {string=} paginationToken token for the next batch of events
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*
*/
Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
timeline, paginationToken) {
if (!timeline) {
throw new Error(
"'timeline' not specified for Room.addEventsToTimeline"
); );
}
if (!toStartOfTimeline && timeline == this._liveTimeline) { // alternatively, we could try to do something like this to try and re-paginate
throw new Error( // in the filtered events from nothing, but Mark says it's an abuse of the API
"Room.addEventsToTimeline cannot be used for adding events to " + // to do so:
"the live timeline - use Room.addLiveEvents instead" //
); // timelineSet.resetLiveTimeline(
} // unfilteredLiveTimeline.getPaginationToken(EventTimeline.FORWARDS)
// );
var direction = toStartOfTimeline ? EventTimeline.BACKWARDS : return timelineSet;
EventTimeline.FORWARDS;
var inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS :
EventTimeline.BACKWARDS;
// Adding events to timelines can be quite complicated. The following
// illustrates some of the corner-cases.
//
// Let's say we start by knowing about four timelines. timeline3 and
// timeline4 are neighbours:
//
// timeline1 timeline2 timeline3 timeline4
// [M] [P] [S] <------> [T]
//
// Now we paginate timeline1, and get the following events from the server:
// [M, N, P, R, S, T, U].
//
// 1. First, we ignore event M, since we already know about it.
//
// 2. Next, we append N to timeline 1.
//
// 3. Next, we don't add event P, since we already know about it,
// but we do link together the timelines. We now have:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P] [S] <------> [T]
//
// 4. Now we add event R to timeline2:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P, R] [S] <------> [T]
//
// Note that we have switched the timeline we are working on from
// timeline1 to timeline2.
//
// 5. We ignore event S, but again join the timelines:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P, R] <---> [S] <------> [T]
//
// 6. We ignore event T, and the timelines are already joined, so there
// is nothing to do.
//
// 7. Finally, we add event U to timeline4:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P, R] <---> [S] <------> [T, U]
//
// The important thing to note in the above is what happened when we
// already knew about a given event:
//
// - if it was appropriate, we joined up the timelines (steps 3, 5).
// - in any case, we started adding further events to the timeline which
// contained the event we knew about (steps 3, 5, 6).
//
//
// So much for adding events to the timeline. But what do we want to do
// with the pagination token?
//
// In the case above, we will be given a pagination token which tells us how to
// get events beyond 'U' - in this case, it makes sense to store this
// against timeline4. But what if timeline4 already had 'U' and beyond? in
// that case, our best bet is to throw away the pagination token we were
// given and stick with whatever token timeline4 had previously. In short,
// we want to only store the pagination token if the last event we receive
// is one we didn't previously know about.
//
// We make an exception for this if it turns out that we already knew about
// *all* of the events, and we weren't able to join up any timelines. When
// that happens, it means our existing pagination token is faulty, since it
// is only telling us what we already know. Rather than repeatedly
// paginating with the same token, we might as well use the new pagination
// token in the hope that we eventually work our way out of the mess.
var didUpdate = false;
var lastEventWasNew = false;
for (var i = 0; i < events.length; i++) {
var event = events[i];
var eventId = event.getId();
var existingTimeline = this._eventIdToTimeline[eventId];
if (!existingTimeline) {
// we don't know about this event yet. Just add it to the timeline.
this._addEventToTimeline(event, timeline, toStartOfTimeline);
lastEventWasNew = true;
didUpdate = true;
continue;
}
lastEventWasNew = false;
if (existingTimeline == timeline) {
debuglog("Event " + eventId + " already in timeline " + timeline);
continue;
}
var neighbour = timeline.getNeighbouringTimeline(direction);
if (neighbour) {
// this timeline already has a neighbour in the relevant direction;
// let's assume the timelines are already correctly linked up, and
// skip over to it.
//
// there's probably some edge-case here where we end up with an
// event which is in a timeline a way down the chain, and there is
// a break in the chain somewhere. But I can't really imagine how
// that would happen, so I'm going to ignore it for now.
//
if (existingTimeline == neighbour) {
debuglog("Event " + eventId + " in neighbouring timeline - " +
"switching to " + existingTimeline);
} else {
debuglog("Event " + eventId + " already in a different " +
"timeline " + existingTimeline);
}
timeline = existingTimeline;
continue;
}
// time to join the timelines.
console.info("Already have timeline for " + eventId +
" - joining timeline " + timeline + " to " +
existingTimeline);
timeline.setNeighbouringTimeline(existingTimeline, direction);
existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
timeline = existingTimeline;
didUpdate = true;
}
// see above - if the last event was new to us, or if we didn't find any
// new information, we update the pagination token for whatever
// timeline we ended up on.
if (lastEventWasNew || !didUpdate) {
timeline.setPaginationToken(paginationToken, direction);
}
}; };
/** /**
* Add event to the given timeline, and emit Room.timeline. Assumes * Forget the timelineSet for this room with the given filter
* we have already checked we don't know about this event.
* *
* Will fire "Room.timeline" for each event added. * @param {Filter} filter the filter whose timelineSet is to be forgotten
*
* @param {MatrixEvent} event
* @param {EventTimeline} timeline
* @param {boolean} toStartOfTimeline
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*
* @private
*/ */
Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline) { Room.prototype.removeFilteredTimelineSet = function(filter) {
var eventId = event.getId(); var timelineSet = this._filteredTimelineSets[filter.filterId];
timeline.addEvent(event, toStartOfTimeline); delete this._filteredTimelineSets[filter.filterId];
this._eventIdToTimeline[eventId] = timeline; var i = this._timelineSets.indexOf(timelineSet);
if (i > -1) {
var data = { this._timelineSets.splice(i, 1);
timeline: timeline, }
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
};
this.emit("Room.timeline", event, this, Boolean(toStartOfTimeline), false, data);
}; };
/** /**
* Add an event to the end of this room's live timeline. Will fire * Add an event to the end of this room's live timelines. Will fire
* "Room.timeline".. * "Room.timeline".
* *
* @param {MatrixEvent} event Event to be added * @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace' * @param {string?} duplicateStrategy 'ignore' or 'replace'
@@ -663,11 +525,12 @@ Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline
* @private * @private
*/ */
Room.prototype._addLiveEvent = function(event, duplicateStrategy) { Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
var i;
if (event.getType() === "m.room.redaction") { if (event.getType() === "m.room.redaction") {
var redactId = event.event.redacts; var redactId = event.event.redacts;
// if we know about this event, redact its contents now. // if we know about this event, redact its contents now.
var redactedEvent = this.findEventById(redactId); var redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
if (redactedEvent) { if (redactedEvent) {
redactedEvent.makeRedacted(event); redactedEvent.makeRedacted(event);
this.emit("Room.redaction", event, this); this.emit("Room.redaction", event, this);
@@ -679,6 +542,8 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
// they are based on are changed. // they are based on are changed.
} }
// FIXME: apply redactions to notification list
// NB: We continue to add the redaction event to the timeline so // NB: We continue to add the redaction event to the timeline so
// clients can say "so and so redacted an event" if they wish to. Also // clients can say "so and so redacted an event" if they wish to. Also
// this may be needed to trigger an update. // this may be needed to trigger an update.
@@ -693,39 +558,11 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
} }
} }
var timeline = this._eventIdToTimeline[event.getId()]; // add to our timeline sets
if (timeline) { for (i = 0; i < this._timelineSets.length; i++) {
if (duplicateStrategy === "replace") { this._timelineSets[i].addLiveEvent(event, duplicateStrategy);
debuglog("Room._addLiveEvent: replacing duplicate event " +
event.getId());
var tlEvents = timeline.getEvents();
for (var j = 0; j < tlEvents.length; j++) {
if (tlEvents[j].getId() === event.getId()) {
// still need to set the right metadata on this event
setEventMetadata(
event,
timeline.getState(EventTimeline.FORWARDS),
false
);
if (!tlEvents[j].encryptedType) {
tlEvents[j] = event;
} }
// XXX: we need to fire an event when this happens.
break;
}
}
} else {
debuglog("Room._addLiveEvent: ignoring duplicate event " +
event.getId());
}
return;
}
// TODO: pass through filter to see if this should be added to the timeline.
this._addEventToTimeline(event, this._liveTimeline, false);
// synthesize and inject implicit read receipts // synthesize and inject implicit read receipts
// Done after adding the event because otherwise the app would get a read receipt // Done after adding the event because otherwise the app would get a read receipt
// pointing to an event that wasn't yet in the timeline // pointing to an event that wasn't yet in the timeline
@@ -773,9 +610,11 @@ Room.prototype.addPendingEvent = function(event, txnId) {
} }
// call setEventMetadata to set up event.sender etc // call setEventMetadata to set up event.sender etc
setEventMetadata( // as event is shared over all timelineSets, we set up its metadata based
// on the unfiltered timelineSet.
EventTimeline.setEventMetadata(
event, event,
this._liveTimeline.getState(EventTimeline.FORWARDS), this.getLiveTimeline().getState(EventTimeline.FORWARDS),
false false
); );
@@ -784,7 +623,19 @@ Room.prototype.addPendingEvent = function(event, txnId) {
if (this._opts.pendingEventOrdering == "detached") { if (this._opts.pendingEventOrdering == "detached") {
this._pendingEventList.push(event); this._pendingEventList.push(event);
} else { } else {
this._addEventToTimeline(event, this._liveTimeline, false); for (var i = 0; i < this._timelineSets.length; i++) {
var timelineSet = this._timelineSets[i];
if (timelineSet.getFilter()) {
if (this._filter.filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
}
else {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
}
} }
this.emit("Room.localEchoUpdated", event, this, null, null); this.emit("Room.localEchoUpdated", event, this, null, null);
@@ -828,13 +679,11 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
// successfully sent. // successfully sent.
localEvent.status = null; localEvent.status = null;
for (var i = 0; i < this._timelineSets.length; i++) {
var timelineSet = this._timelineSets[i];
// if it's already in the timeline, update the timeline map. If it's not, add it. // if it's already in the timeline, update the timeline map. If it's not, add it.
var existingTimeline = this._eventIdToTimeline[oldEventId]; timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
} else {
this._addEventToTimeline(localEvent, this._liveTimeline, false);
} }
this.emit("Room.localEchoUpdated", localEvent, this, this.emit("Room.localEchoUpdated", localEvent, this,
@@ -890,7 +739,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
// SENT races against /sync, so we have to special-case it. // SENT races against /sync, so we have to special-case it.
if (newStatus == EventStatus.SENT) { if (newStatus == EventStatus.SENT) {
var timeline = this._eventIdToTimeline[newEventId]; var timeline = this.getUnfilteredTimelineSet().eventIdToTimeline(newEventId);
if (timeline) { if (timeline) {
// we've already received the event via the event stream. // we've already received the event via the event stream.
// nothing more to do here. // nothing more to do here.
@@ -921,10 +770,8 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
// if the event was already in the timeline (which will be the case if // if the event was already in the timeline (which will be the case if
// opts.pendingEventOrdering==chronological), we need to update the // opts.pendingEventOrdering==chronological), we need to update the
// timeline map. // timeline map.
var existingTimeline = this._eventIdToTimeline[oldEventId]; for (var i = 0; i < this._timelineSets.length; i++) {
if (existingTimeline) { this._timelineSets[i].replaceEventId(oldEventId, newEventId);
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
} }
} }
else if (newStatus == EventStatus.CANCELLED) { else if (newStatus == EventStatus.CANCELLED) {
@@ -960,24 +807,29 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'. * @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
*/ */
Room.prototype.addLiveEvents = function(events, duplicateStrategy) { Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
var i;
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) { if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'"); throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
} }
// sanity check that the live timeline is still live // sanity check that the live timeline is still live
if (this._liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) { for (i = 0; i < this._timelineSets.length; i++) {
var liveTimeline = this._timelineSets[i].getLiveTimeline();
if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
throw new Error( throw new Error(
"live timeline is no longer live - it has a pagination token (" + "live timeline " + i + " is no longer live - it has a pagination token " +
this._liveTimeline.getPaginationToken(EventTimeline.FORWARDS) + ")" "(" + liveTimeline.getPaginationToken(EventTimeline.FORWARDS) + ")"
); );
} }
if (this._liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) { if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
throw new Error( throw new Error(
"live timeline is no longer live - it has a neighbouring timeline" "live timeline " + i + " is no longer live - " +
"it has a neighbouring timeline"
); );
} }
}
for (var i = 0; i < events.length; i++) { for (i = 0; i < events.length; i++) {
if (events[i].getType() === "m.typing") { if (events[i].getType() === "m.typing") {
this.currentState.setTypingEvent(events[i]); this.currentState.setTypingEvent(events[i]);
} }
@@ -1009,98 +861,19 @@ Room.prototype.removeEvents = function(event_ids) {
* *
* @param {String} eventId The id of the event to remove * @param {String} eventId The id of the event to remove
* *
* @return {?MatrixEvent} the removed event, or null if the event was not found * @return {bool} true if the event was removed from any of the room's timeline sets
* in this room.
*/ */
Room.prototype.removeEvent = function(eventId) { Room.prototype.removeEvent = function(eventId) {
var timeline = this._eventIdToTimeline[eventId]; var removedAny = false;
if (!timeline) { for (var i = 0; i < this._timelineSets.length; i++) {
return null; var removed = this._timelineSets[i].removeEvent(eventId);
}
var removed = timeline.removeEvent(eventId);
if (removed) { if (removed) {
delete this._eventIdToTimeline[eventId]; removedAny = true;
var data = {
timeline: timeline,
};
this.emit("Room.timeline", removed, this, undefined, true, data);
} }
return removed; }
return removedAny;
}; };
/**
* Determine where two events appear in the timeline relative to one another
*
* @param {string} eventId1 The id of the first event
* @param {string} eventId2 The id of the second event
* @return {?number} a number less than zero if eventId1 precedes eventId2, and
* greater than zero if eventId1 succeeds eventId2. zero if they are the
* same event; null if we can't tell (either because we don't know about one
* of the events, or because they are in separate timelines which don't join
* up).
*/
Room.prototype.compareEventOrdering = function(eventId1, eventId2) {
if (eventId1 == eventId2) {
// optimise this case
return 0;
}
var timeline1 = this._eventIdToTimeline[eventId1];
var timeline2 = this._eventIdToTimeline[eventId2];
if (timeline1 === undefined) {
return null;
}
if (timeline2 === undefined) {
return null;
}
if (timeline1 === timeline2) {
// both events are in the same timeline - figure out their
// relative indices
var idx1, idx2;
var events = timeline1.getEvents();
for (var idx = 0; idx < events.length &&
(idx1 === undefined || idx2 === undefined); idx++) {
var evId = events[idx].getId();
if (evId == eventId1) {
idx1 = idx;
}
if (evId == eventId2) {
idx2 = idx;
}
}
return idx1 - idx2;
}
// the events are in different timelines. Iterate through the
// linkedlist to see which comes first.
// first work forwards from timeline1
var tl = timeline1;
while (tl) {
if (tl === timeline2) {
// timeline1 is before timeline2
return -1;
}
tl = tl.getNeighbouringTimeline(EventTimeline.FORWARDS);
}
// now try backwards from timeline1
tl = timeline1;
while (tl) {
if (tl === timeline2) {
// timeline2 is before timeline1
return 1;
}
tl = tl.getNeighbouringTimeline(EventTimeline.BACKWARDS);
}
// the timelines are not contiguous.
return null;
};
/** /**
* Recalculate various aspects of the room, including the room name and * Recalculate various aspects of the room, including the room name and
@@ -1221,7 +994,7 @@ Room.prototype.addReceipt = function(event, fake) {
// as there's nothing that would read it. // as there's nothing that would read it.
} }
this._addReceiptsToStructure(event, this._receipts); this._addReceiptsToStructure(event, this._receipts);
this._receiptCacheByEventId = this._buildReciptCache(this._receipts); this._receiptCacheByEventId = this._buildReceiptCache(this._receipts);
// send events after we've regenerated the cache, otherwise things that // send events after we've regenerated the cache, otherwise things that
// listened for the event would read from a stale cache // listened for the event would read from a stale cache
@@ -1254,7 +1027,7 @@ Room.prototype._addReceiptsToStructure = function(event, receipts) {
// than the one we already have. (This is managed // than the one we already have. (This is managed
// server-side, but because we synthesize RRs locally we // server-side, but because we synthesize RRs locally we
// have to do it here too.) // have to do it here too.)
var ordering = self.compareEventOrdering( var ordering = self.getUnfilteredTimelineSet().compareEventOrdering(
existingReceipt.eventId, eventId); existingReceipt.eventId, eventId);
if (ordering !== null && ordering >= 0) { if (ordering !== null && ordering >= 0) {
return; return;
@@ -1275,7 +1048,7 @@ Room.prototype._addReceiptsToStructure = function(event, receipts) {
* @param {Object} receipts A map of receipts * @param {Object} receipts A map of receipts
* @return {Object} Map of receipts by event ID * @return {Object} Map of receipts by event ID
*/ */
Room.prototype._buildReciptCache = function(receipts) { Room.prototype._buildReceiptCache = function(receipts) {
var receiptCacheByEventId = {}; var receiptCacheByEventId = {};
utils.keys(receipts).forEach(function(receiptType) { utils.keys(receipts).forEach(function(receiptType) {
utils.keys(receipts[receiptType]).forEach(function(userId) { utils.keys(receipts[receiptType]).forEach(function(userId) {
@@ -1350,27 +1123,6 @@ Room.prototype.getAccountData = function(type) {
return this.accountData[type]; return this.accountData[type];
}; };
function setEventMetadata(event, stateContext, toStartOfTimeline) {
// set sender and target properties
event.sender = stateContext.getSentinelMember(
event.getSender()
);
if (event.getType() === "m.room.member") {
event.target = stateContext.getSentinelMember(
event.getStateKey()
);
}
if (event.isState()) {
// room state has no concept of 'old' or 'current', but we want the
// room state to regress back to previous values if toStartOfTimeline
// is set, which means inspecting prev_content if it exists. This
// is done by toggling the forwardLooking flag.
if (toStartOfTimeline) {
event.forwardLooking = false;
}
}
}
/** /**
* This is an internal method. Calculates the name of the room from the current * This is an internal method. Calculates the name of the room from the current
* room state. * room state.
@@ -1486,49 +1238,30 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
} }
} }
// FIXME: copypasted from sync.js
function reEmit(reEmitEntity, emittableEntity, eventNames) {
utils.forEach(eventNames, function(eventName) {
// setup a listener on the entity (the Room, User, etc) for this event
emittableEntity.on(eventName, function() {
// take the args from the listener and reuse them, adding the
// event name to the arg list so it works with .emit()
// Transformation Example:
// listener on "foo" => function(a,b) { ... }
// Re-emit on "thing" => thing.emit("foo", a, b)
var newArgs = [eventName];
for (var i = 0; i < arguments.length; i++) {
newArgs.push(arguments[i]);
}
reEmitEntity.emit.apply(reEmitEntity, newArgs);
});
});
}
/** /**
* The Room class. * The Room class.
*/ */
module.exports = Room; module.exports = Room;
/**
* Fires whenever the timeline in a room is updated.
* @event module:client~MatrixClient#"Room.timeline"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {Room} room The room whose Room.timeline was updated.
* @param {boolean} toStartOfTimeline True if this event was added to the start
* @param {boolean} removed True if this event has just been removed from the timeline
* (beginning; oldest) of the timeline e.g. due to pagination.
*
* @param {object} data more data about the event
*
* @param {module:event-timeline.EventTimeline} data.timeline the timeline the
* event was added to/removed from
*
* @param {boolean} data.liveEvent true if the event was a real-time event
* added to the end of the live timeline
*
* @example
* matrixClient.on("Room.timeline",
* function(event, room, toStartOfTimeline, removed, data) {
* if (!toStartOfTimeline && data.liveEvent) {
* var messageToAppend = room.timeline.[room.timeline.length - 1];
* }
* });
*/
/**
* Fires whenever the live timeline in a room is reset.
*
* When we get a 'limited' sync (for example, after a network outage), we reset
* the live timeline to be empty before adding the recent events to the new
* timeline. This event is fired after the timeline is reset, and before the
* new events are added.
*
* @event module:client~MatrixClient#"Room.timelineReset"
* @param {Room} room The room whose live timeline was reset.
*/
/** /**
* Fires when an event we had previously received is redacted. * Fires when an event we had previously received is redacted.
* *

View File

@@ -72,6 +72,12 @@ function SyncApi(client, opts) {
this._running = false; this._running = false;
this._keepAliveTimer = null; this._keepAliveTimer = null;
this._connectionReturnedDefer = null; this._connectionReturnedDefer = null;
this._notifEvents = []; // accumulator of sync events in the current sync response
if (client.getNotifTimelineSet()) {
reEmit(client, client.getNotifTimelineSet(),
["Room.timeline", "Room.timelineReset"]);
}
} }
/** /**
@@ -148,7 +154,7 @@ SyncApi.prototype.syncLeftRooms = function() {
timeout: 0 // don't want to block since this is a single isolated req timeout: 0 // don't want to block since this is a single isolated req
}; };
return this._getOrCreateFilter( return client.getOrCreateFilter(
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter
).then(function(filterId) { ).then(function(filterId) {
qps.filter = filterId; qps.filter = filterId;
@@ -389,9 +395,15 @@ SyncApi.prototype.sync = function() {
var filter = new Filter(client.credentials.userId); var filter = new Filter(client.credentials.userId);
filter.setTimelineLimit(self.opts.initialSyncLimit); filter.setTimelineLimit(self.opts.initialSyncLimit);
self._getOrCreateFilter( client.getOrCreateFilter(
getFilterName(client.credentials.userId), filter getFilterName(client.credentials.userId), filter
).done(function(filterId) { ).done(function(filterId) {
// reset the notifications timeline to prepare it to paginate from
// the current point in time.
// The right solution would be to tie /sync pagination tokens into
// /notifications API somehow.
client.resetNotifTimelineSet();
self._sync({ filterId: filterId }); self._sync({ filterId: filterId });
}, function(err) { }, function(err) {
self._startKeepAlives().done(function() { self._startKeepAlives().done(function() {
@@ -666,6 +678,8 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
} }
} }
this._notifEvents = [];
// Handle invites // Handle invites
inviteRooms.forEach(function(inviteObj) { inviteRooms.forEach(function(inviteObj) {
var room = inviteObj.room; var room = inviteObj.room;
@@ -751,6 +765,12 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
room.currentState.paginationToken = syncToken; room.currentState.paginationToken = syncToken;
self._deregisterStateListeners(room); self._deregisterStateListeners(room);
room.resetLiveTimeline(joinObj.timeline.prev_batch); room.resetLiveTimeline(joinObj.timeline.prev_batch);
// We have to assume any gap in any timeline is
// reason to stop incrementally tracking notifications and
// reset the timeline.
client.resetNotifTimelineSet();
self._registerStateListeners(room); self._registerStateListeners(room);
} }
} }
@@ -799,6 +819,20 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
timelineEvents.forEach(function(e) { client.emit("event", e); }); timelineEvents.forEach(function(e) { client.emit("event", e); });
accountDataEvents.forEach(function(e) { client.emit("event", e); }); accountDataEvents.forEach(function(e) { client.emit("event", e); });
}); });
// update the notification timeline, if appropriate.
// we only do this for live events, as otherwise we can't order them sanely
// in the timeline relative to ones paginated in by /notifications.
// XXX: we could fix this by making EventTimeline support chronological
// ordering... but it doesn't, right now.
if (syncToken && this._notifEvents.length) {
this._notifEvents.sort(function(a, b) {
return a.getTs() - b.getTs();
});
this._notifEvents.forEach(function(event) {
client.getNotifTimelineSet().addLiveEvent(event);
});
}
}; };
/** /**
@@ -862,53 +896,6 @@ SyncApi.prototype._pokeKeepAlive = function() {
}); });
}; };
/**
* @param {string} filterName
* @param {Filter} filter
* @return {Promise<String>} Filter ID
*/
SyncApi.prototype._getOrCreateFilter = function(filterName, filter) {
var client = this.client;
var filterId = client.store.getFilterIdByName(filterName);
var promise = q();
if (filterId) {
// check that the existing filter matches our expectations
promise = client.getFilter(client.credentials.userId,
filterId, true
).then(function(existingFilter) {
var oldDef = existingFilter.getDefinition();
var newDef = filter.getDefinition();
if (utils.deepCompare(oldDef, newDef)) {
// super, just use that.
debuglog("Using existing filter ID %s: %s", filterId,
JSON.stringify(oldDef));
return q(filterId);
}
debuglog("Existing filter ID %s: %s; new filter: %s",
filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
return;
});
}
return promise.then(function(existingId) {
if (existingId) {
return existingId;
}
// create a new filter
return client.createFilter(filter.getDefinition()
).then(function(createdFilter) {
debuglog("Created new filter ID %s: %s", createdFilter.filterId,
JSON.stringify(createdFilter.getDefinition()));
client.store.setFilterIdByName(filterName, createdFilter.filterId);
return createdFilter.filterId;
});
});
};
/** /**
* @param {Object} obj * @param {Object} obj
* @return {Object[]} * @return {Object[]}
@@ -1034,6 +1021,18 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
// may make notifications appear which should have the right name. // may make notifications appear which should have the right name.
room.recalculate(this.client.credentials.userId); room.recalculate(this.client.credentials.userId);
// gather our notifications into this._notifEvents
if (client.getNotifTimelineSet()) {
for (var i = 0; i < timelineEventList.length; i++) {
var pushActions = client.getPushActionsForEvent(timelineEventList[i]);
if (pushActions && pushActions.notify &&
pushActions.tweaks && pushActions.tweaks.highlight)
{
this._notifEvents.push(timelineEventList[i]);
}
}
}
// execute the timeline events, this will begin to diverge the current state // execute the timeline events, this will begin to diverge the current state
// if the timeline has any state events in it. // if the timeline has any state events in it.
room.addLiveEvents(timelineEventList); room.addLiveEvents(timelineEventList);

View File

@@ -56,7 +56,7 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
* @param {MatrixClient} client MatrixClient to be used for context/pagination * @param {MatrixClient} client MatrixClient to be used for context/pagination
* requests. * requests.
* *
* @param {Room} room The room to track * @param {EventTimelineSet} timelineSet The timelineSet to track
* *
* @param {Object} [opts] Configuration options for this window * @param {Object} [opts] Configuration options for this window
* *
@@ -66,10 +66,10 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
* *
* @constructor * @constructor
*/ */
function TimelineWindow(client, room, opts) { function TimelineWindow(client, timelineSet, opts) {
opts = opts || {}; opts = opts || {};
this._client = client; this._client = client;
this._room = room; this._timelineSet = timelineSet;
// these will be TimelineIndex objects; they delineate the 'start' and // these will be TimelineIndex objects; they delineate the 'start' and
// 'end' of the window. // 'end' of the window.
@@ -113,7 +113,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
// TODO: ideally we'd spot getEventTimeline returning a resolved promise and // TODO: ideally we'd spot getEventTimeline returning a resolved promise and
// skip straight to the find-event loop. // skip straight to the find-event loop.
if (initialEventId) { if (initialEventId) {
return this._client.getEventTimeline(this._room, initialEventId) return this._client.getEventTimeline(this._timelineSet, initialEventId)
.then(function(tl) { .then(function(tl) {
// make sure that our window includes the event // make sure that our window includes the event
for (var i = 0; i < tl.getEvents().length; i++) { for (var i = 0; i < tl.getEvents().length; i++) {
@@ -126,7 +126,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
}); });
} else { } else {
// start with the most recent events // start with the most recent events
var tl = this._room.getLiveTimeline(); var tl = this._timelineSet.getLiveTimeline();
initFields(tl, tl.getEvents().length); initFields(tl, tl.getEvents().length);
return q(); return q();
} }
@@ -254,6 +254,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
debuglog("TimelineWindow: starting request"); debuglog("TimelineWindow: starting request");
var self = this; var self = this;
var prom = this._client.paginateEventTimeline(tl.timeline, { var prom = this._client.paginateEventTimeline(tl.timeline, {
backwards: direction == EventTimeline.BACKWARDS, backwards: direction == EventTimeline.BACKWARDS,
limit: size limit: size

View File

@@ -121,7 +121,8 @@ describe("getEventTimeline support", function() {
startClient(httpBackend, client startClient(httpBackend, client
).then(function() { ).then(function() {
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
expect(function() { client.getEventTimeline(room, "event"); }) var timelineSet = room.getTimelineSets()[0];
expect(function() { client.getEventTimeline(timelineSet, "event"); })
.toThrow(); .toThrow();
}).catch(utils.failTest).done(done); }).catch(utils.failTest).done(done);
}); });
@@ -137,7 +138,8 @@ describe("getEventTimeline support", function() {
startClient(httpBackend, client startClient(httpBackend, client
).then(function() { ).then(function() {
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
expect(function() { client.getEventTimeline(room, "event"); }) var timelineSet = room.getTimelineSets()[0];
expect(function() { client.getEventTimeline(timelineSet, "event"); })
.not.toThrow(); .not.toThrow();
}).catch(utils.failTest).done(done); }).catch(utils.failTest).done(done);
@@ -242,6 +244,7 @@ describe("MatrixClient event timelines", function() {
describe("getEventTimeline", function() { describe("getEventTimeline", function() {
it("should create a new timeline for new events", function(done) { it("should create a new timeline for new events", function(done) {
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar") httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
.respond(200, function() { .respond(200, function() {
return { return {
@@ -257,7 +260,7 @@ describe("MatrixClient event timelines", function() {
}; };
}); });
client.getEventTimeline(room, "event1:bar").then(function(tl) { client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
expect(tl.getEvents().length).toEqual(4); expect(tl.getEvents().length).toEqual(4);
for (var i = 0; i < 4; i++) { for (var i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]); expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
@@ -274,6 +277,7 @@ describe("MatrixClient event timelines", function() {
it("should return existing timeline for known events", function(done) { it("should return existing timeline for known events", function(done) {
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, { httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4", next_batch: "s_5_4",
rooms: { rooms: {
@@ -291,7 +295,7 @@ describe("MatrixClient event timelines", function() {
}); });
httpBackend.flush("/sync").then(function() { httpBackend.flush("/sync").then(function() {
return client.getEventTimeline(room, EVENTS[0].event_id); return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
}).then(function(tl) { }).then(function(tl) {
expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]); expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
@@ -305,6 +309,7 @@ describe("MatrixClient event timelines", function() {
it("should update timelines where they overlap a previous /sync", function(done) { it("should update timelines where they overlap a previous /sync", function(done) {
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, { httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4", next_batch: "s_5_4",
rooms: { rooms: {
@@ -335,7 +340,7 @@ describe("MatrixClient event timelines", function() {
}); });
client.on("sync", function() { client.on("sync", function() {
client.getEventTimeline(room, EVENTS[2].event_id client.getEventTimeline(timelineSet, EVENTS[2].event_id
).then(function(tl) { ).then(function(tl) {
expect(tl.getEvents().length).toEqual(4); expect(tl.getEvents().length).toEqual(4);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]); expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
@@ -354,6 +359,7 @@ describe("MatrixClient event timelines", function() {
it("should join timelines where they overlap a previous /context", it("should join timelines where they overlap a previous /context",
function(done) { function(done) {
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned // we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up. // with context which joins them all up.
@@ -410,19 +416,19 @@ describe("MatrixClient event timelines", function() {
}); });
var tl0, tl2, tl3; var tl0, tl2, tl3;
client.getEventTimeline(room, EVENTS[0].event_id client.getEventTimeline(timelineSet, EVENTS[0].event_id
).then(function(tl) { ).then(function(tl) {
expect(tl.getEvents().length).toEqual(1); expect(tl.getEvents().length).toEqual(1);
tl0 = tl; tl0 = tl;
return client.getEventTimeline(room, EVENTS[2].event_id); return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
}).then(function(tl) { }).then(function(tl) {
expect(tl.getEvents().length).toEqual(1); expect(tl.getEvents().length).toEqual(1);
tl2 = tl; tl2 = tl;
return client.getEventTimeline(room, EVENTS[3].event_id); return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
}).then(function(tl) { }).then(function(tl) {
expect(tl.getEvents().length).toEqual(1); expect(tl.getEvents().length).toEqual(1);
tl3 = tl; tl3 = tl;
return client.getEventTimeline(room, EVENTS[1].event_id); return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
}).then(function(tl) { }).then(function(tl) {
// we expect it to get merged in with event 2 // we expect it to get merged in with event 2
expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents().length).toEqual(2);
@@ -447,6 +453,7 @@ describe("MatrixClient event timelines", function() {
it("should fail gracefully if there is no event field", function(done) { it("should fail gracefully if there is no event field", function(done) {
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned // we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up. // with context which joins them all up.
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1") httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
@@ -460,7 +467,7 @@ describe("MatrixClient event timelines", function() {
}; };
}); });
client.getEventTimeline(room, "event1" client.getEventTimeline(timelineSet, "event1"
).then(function(tl) { ).then(function(tl) {
// could do with a fail() // could do with a fail()
expect(true).toBeFalsy(); expect(true).toBeFalsy();
@@ -475,6 +482,7 @@ describe("MatrixClient event timelines", function() {
describe("paginateEventTimeline", function() { describe("paginateEventTimeline", function() {
it("should allow you to paginate backwards", function(done) { it("should allow you to paginate backwards", function(done) {
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id)) encodeURIComponent(EVENTS[0].event_id))
@@ -503,7 +511,7 @@ describe("MatrixClient event timelines", function() {
}); });
var tl; var tl;
client.getEventTimeline(room, EVENTS[0].event_id client.getEventTimeline(timelineSet, EVENTS[0].event_id
).then(function(tl0) { ).then(function(tl0) {
tl = tl0; tl = tl0;
return client.paginateEventTimeline(tl, {backwards: true}); return client.paginateEventTimeline(tl, {backwards: true});
@@ -525,6 +533,7 @@ describe("MatrixClient event timelines", function() {
it("should allow you to paginate forwards", function(done) { it("should allow you to paginate forwards", function(done) {
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" + httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id)) encodeURIComponent(EVENTS[0].event_id))
@@ -553,7 +562,7 @@ describe("MatrixClient event timelines", function() {
}); });
var tl; var tl;
client.getEventTimeline(room, EVENTS[0].event_id client.getEventTimeline(timelineSet, EVENTS[0].event_id
).then(function(tl0) { ).then(function(tl0) {
tl = tl0; tl = tl0;
return client.paginateEventTimeline( return client.paginateEventTimeline(
@@ -607,10 +616,11 @@ describe("MatrixClient event timelines", function() {
it("should work when /send returns before /sync", function(done) { it("should work when /send returns before /sync", function(done) {
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) { client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
expect(res.event_id).toEqual(event.event_id); expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(room, event.event_id); return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) { }).then(function(tl) {
// 2 because the initial sync contained an event // 2 because the initial sync contained an event
expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents().length).toEqual(2);
@@ -619,7 +629,7 @@ describe("MatrixClient event timelines", function() {
// now let the sync complete, and check it again // now let the sync complete, and check it again
return httpBackend.flush("/sync", 1); return httpBackend.flush("/sync", 1);
}).then(function() { }).then(function() {
return client.getEventTimeline(room, event.event_id); return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) { }).then(function(tl) {
expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event); expect(tl.getEvents()[1].event).toEqual(event);
@@ -630,13 +640,14 @@ describe("MatrixClient event timelines", function() {
it("should work when /send returns after /sync", function(done) { it("should work when /send returns after /sync", function(done) {
var room = client.getRoom(roomId); var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
// initiate the send, and set up checks to be done when it completes // initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below. // - but note that it won't complete until after the /sync does, below.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) { client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
console.log("sendTextMessage completed"); console.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id); expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(room, event.event_id); return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) { }).then(function(tl) {
console.log("getEventTimeline completed (2)"); console.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents().length).toEqual(2);
@@ -644,7 +655,7 @@ describe("MatrixClient event timelines", function() {
}).catch(utils.failTest).done(done); }).catch(utils.failTest).done(done);
httpBackend.flush("/sync", 1).then(function() { httpBackend.flush("/sync", 1).then(function() {
return client.getEventTimeline(room, event.event_id); return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) { }).then(function(tl) {
console.log("getEventTimeline completed (1)"); console.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2); expect(tl.getEvents().length).toEqual(2);

View File

@@ -16,7 +16,12 @@ describe("EventTimeline", function() {
beforeEach(function() { beforeEach(function() {
utils.beforeEach(this); utils.beforeEach(this);
timeline = new EventTimeline(roomId);
// XXX: this is a horrid hack; should use sinon or something instead to mock
var timelineSet = { room: { roomId: roomId }};
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
timeline = new EventTimeline(timelineSet);
}); });
describe("construction", function() { describe("construction", function() {

View File

@@ -479,13 +479,13 @@ describe("Room", function() {
it("should handle events in the same timeline", function() { it("should handle events in the same timeline", function() {
room.addLiveEvents(events); room.addLiveEvents(events);
expect(room.compareEventOrdering(events[0].getId(), expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
events[1].getId())) events[1].getId()))
.toBeLessThan(0); .toBeLessThan(0);
expect(room.compareEventOrdering(events[2].getId(), expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId(),
events[1].getId())) events[1].getId()))
.toBeGreaterThan(0); .toBeGreaterThan(0);
expect(room.compareEventOrdering(events[1].getId(), expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
events[1].getId())) events[1].getId()))
.toEqual(0); .toEqual(0);
}); });
@@ -498,10 +498,10 @@ describe("Room", function() {
room.addEventsToTimeline([events[0]], false, oldTimeline); room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addLiveEvents([events[1]]); room.addLiveEvents([events[1]]);
expect(room.compareEventOrdering(events[0].getId(), expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
events[1].getId())) events[1].getId()))
.toBeLessThan(0); .toBeLessThan(0);
expect(room.compareEventOrdering(events[1].getId(), expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
events[0].getId())) events[0].getId()))
.toBeGreaterThan(0); .toBeGreaterThan(0);
}); });
@@ -512,10 +512,10 @@ describe("Room", function() {
room.addEventsToTimeline([events[0]], false, oldTimeline); room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addLiveEvents([events[1]]); room.addLiveEvents([events[1]]);
expect(room.compareEventOrdering(events[0].getId(), expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
events[1].getId())) events[1].getId()))
.toBe(null); .toBe(null);
expect(room.compareEventOrdering(events[1].getId(), expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
events[0].getId())) events[0].getId()))
.toBe(null); .toBe(null);
}); });
@@ -523,12 +523,14 @@ describe("Room", function() {
it("should return null for unknown events", function() { it("should return null for unknown events", function() {
room.addLiveEvents(events); room.addLiveEvents(events);
expect(room.compareEventOrdering(events[0].getId(), "xxx")) expect(room.getUnfilteredTimelineSet()
.compareEventOrdering(events[0].getId(), "xxx"))
.toBe(null); .toBe(null);
expect(room.compareEventOrdering("xxx", events[0].getId())) expect(room.getUnfilteredTimelineSet()
.compareEventOrdering("xxx", events[0].getId()))
.toBe(null); .toBe(null);
expect(room.compareEventOrdering(events[0].getId(), expect(room.getUnfilteredTimelineSet()
events[0].getId())) .compareEventOrdering(events[0].getId(), events[0].getId()))
.toBe(0); .toBe(0);
}); });
}); });

View File

@@ -18,7 +18,11 @@ function createTimeline(numEvents, baseIndex) {
if (numEvents === undefined) { numEvents = 3; } if (numEvents === undefined) { numEvents = 3; }
if (baseIndex === undefined) { baseIndex = 1; } if (baseIndex === undefined) { baseIndex = 1; }
var timeline = new EventTimeline(ROOM_ID); // XXX: this is a horrid hack
var timelineSet = { room: { roomId: ROOM_ID }};
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
var timeline = new EventTimeline(timelineSet);
// add the events after the baseIndex first // add the events after the baseIndex first
addEventsToTimeline(timeline, numEvents - baseIndex, false); addEventsToTimeline(timeline, numEvents - baseIndex, false);
@@ -133,19 +137,19 @@ describe("TimelineIndex", function() {
describe("TimelineWindow", function() { describe("TimelineWindow", function() {
/** /**
* create a dummy room and client, and a TimelineWindow * create a dummy eventTimelineSet and client, and a TimelineWindow
* attached to them. * attached to them.
*/ */
var room, client; var timelineSet, client;
function createWindow(timeline, opts) { function createWindow(timeline, opts) {
room = {}; timelineSet = {};
client = {}; client = {};
client.getEventTimeline = function(room0, eventId0) { client.getEventTimeline = function(timelineSet0, eventId0) {
expect(room0).toBe(room); expect(timelineSet0).toBe(timelineSet);
return q(timeline); return q(timeline);
}; };
return new TimelineWindow(client, room, opts); return new TimelineWindow(client, timelineSet, opts);
} }
beforeEach(function() { beforeEach(function() {
@@ -169,15 +173,15 @@ describe("TimelineWindow", function() {
var timeline = createTimeline(); var timeline = createTimeline();
var eventId = timeline.getEvents()[1].getId(); var eventId = timeline.getEvents()[1].getId();
var room = {}; var timelineSet = {};
var client = {}; var client = {};
client.getEventTimeline = function(room0, eventId0) { client.getEventTimeline = function(timelineSet0, eventId0) {
expect(room0).toBe(room); expect(timelineSet0).toBe(timelineSet);
expect(eventId0).toEqual(eventId); expect(eventId0).toEqual(eventId);
return q(timeline); return q(timeline);
}; };
var timelineWindow = new TimelineWindow(client, room); var timelineWindow = new TimelineWindow(client, timelineSet);
timelineWindow.load(eventId, 3).then(function() { timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents(); var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents); expect(timelineWindow.getEvents()).toEqual(expectedEvents);
@@ -192,12 +196,12 @@ describe("TimelineWindow", function() {
var eventId = timeline.getEvents()[1].getId(); var eventId = timeline.getEvents()[1].getId();
var room = {}; var timelineSet = {};
var client = {}; var client = {};
var timelineWindow = new TimelineWindow(client, room); var timelineWindow = new TimelineWindow(client, timelineSet);
client.getEventTimeline = function(room0, eventId0) { client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS)) expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false); .toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)) expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))