Compare commits
137 Commits
v0.5.6
..
v0.6.0-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
| 425f862cf8 | |||
| 5d6256bede | |||
| ff5b923e6f | |||
| af7a9a68b8 | |||
| 905059d6da | |||
| 3bc56cf3f8 | |||
| 1feb7fc0ba | |||
| c2a40572a5 | |||
| ee7d4d0521 | |||
| 6ab410ef6a | |||
| 8235d966d6 | |||
| c7b83f6ee6 | |||
| 460f20a4ce | |||
| da408f975e | |||
| 9a98c3991a | |||
| 6e0b2de99f | |||
| 0633d7d3f6 | |||
| 2765720b76 | |||
| 71f23ffce1 | |||
| 1863af147d | |||
| 0d5d74674e | |||
| 45ed0884df | |||
| 45e9f59fdc | |||
| bde6a171f6 | |||
| 49a74755a8 | |||
| 2fbef8638f | |||
| eb4166afe3 | |||
| b3beaacec7 | |||
| 355b728a57 | |||
| 577b0e8f1b | |||
| 35d99564c1 | |||
| 6f9bb38232 | |||
| d02c205910 | |||
| 38681202dc | |||
| 0d20a0acf0 | |||
| 9277a86403 | |||
| 5ec8688cf6 | |||
| 6ae82a9cb4 | |||
| 72a4b92022 | |||
| 0cc68bc125 | |||
| 6ca917f4db | |||
| 8a848deddc | |||
| 2ebd4b15a4 | |||
| f0274f3f26 | |||
| 85b2e5d758 | |||
| eef03882ad | |||
| f7e5d962c0 | |||
| 87c6a40b3f | |||
| e614e17a71 | |||
| b4dc5e620b | |||
| 0713e65fc5 | |||
| b69f6cf70a | |||
| 2c6409a67a | |||
| ad7db78829 | |||
| bd9e3e5794 | |||
| bd32ed5598 | |||
| 5a5257a598 | |||
| 75b6ebf287 | |||
| a9d3ae4ef8 | |||
| d480b6cf3e | |||
| fdb640e361 | |||
| 924a8533f1 | |||
| 72b4f270ff | |||
| 946539e32d | |||
| 9882fed6d7 | |||
| 93f45c0a94 | |||
| c6d358a6f3 | |||
| 2e4c362ccd | |||
| f959e1a134 | |||
| 7dfc4a404c | |||
| 2af349eb72 | |||
| 43f3a1e8b3 | |||
| 13c186dfbe | |||
| 4d88736d13 | |||
| 1da633e28a | |||
| 879da47f0e | |||
| cacafb461d | |||
| 15e285c6b4 | |||
| 71c33420f6 | |||
| e7f70bba5c | |||
| e4ec2aa55f | |||
| fc495a5f1e | |||
| 6fe4dfcad0 | |||
| dac820f957 | |||
| 91f8df8d19 | |||
| 9b507f6c6c | |||
| 5e583d3c50 | |||
| d706b57fe9 | |||
| 1063a16013 | |||
| 46a2073427 | |||
| d7bb9574e7 | |||
| 9c18893ae5 | |||
| 4503c320e5 | |||
| c4995bd153 | |||
| af0f5b37d8 | |||
| b9ba4671b4 | |||
| 50b8f13037 | |||
| 4aa9dca608 | |||
| 98dc5328a0 | |||
| 408671b58a | |||
| 1bda527e3d | |||
| e0f1b9ebf0 | |||
| 55127aa43f | |||
| 888fbe3549 | |||
| ed5c061566 | |||
| df6b1d1471 | |||
| 5e0f09075d | |||
| 2daa1b6007 | |||
| 4ff2ad9fac | |||
| ba06e8091f | |||
| 692b3107ac | |||
| 6baf9e1c37 | |||
| c07e662b90 | |||
| aca8b32e5b | |||
| 0ec8a6e0af | |||
| 41af7c8883 | |||
| 7f2070f7b7 | |||
| 1cc74ec116 | |||
| 627f662384 | |||
| c791881c87 | |||
| b0782885d5 | |||
| d25d60f0f0 | |||
| d356e722da | |||
| 84e2fc91ae | |||
| 9768fb020c | |||
| e25112ad35 | |||
| e18b446190 | |||
| 0848d4ed10 | |||
| 7514aea813 | |||
| 58031ab21d | |||
| c1c2ca3ec1 | |||
| b863a363da | |||
| b42db46abd | |||
| d46863e199 | |||
| 751ce421cd | |||
| b4f22310ea | |||
| dd5878015a |
@@ -14,7 +14,7 @@ Copy ``dist/$VERSION/browser-matrix-$VERSION.js`` and add that as a ``<script>``
|
||||
your page. There will be a global variable ``matrixcs`` attached to
|
||||
``window`` through which you can access the SDK.
|
||||
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
|
||||
In Node.js
|
||||
----------
|
||||
@@ -78,7 +78,7 @@ are updated.
|
||||
client.on("event", function(event) {
|
||||
console.log(event.getType());
|
||||
});
|
||||
|
||||
|
||||
// Listen for typing changes
|
||||
client.on("RoomMember.typing", function(event, member) {
|
||||
if (member.typing) {
|
||||
@@ -88,38 +88,43 @@ are updated.
|
||||
console.log(member.name + " stopped typing.");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// start the client to setup the connection to the server
|
||||
client.startClient();
|
||||
```
|
||||
|
||||
### Promises or Callbacks
|
||||
### Promises and Callbacks
|
||||
|
||||
The SDK supports *both* callbacks and Promises (Q). The convention
|
||||
you'll see used is:
|
||||
Most of the methods in the SDK are asynchronous: they do not directly return a
|
||||
result, but instead return a [Promise](http://documentup.com/kriskowal/q/)
|
||||
which will be fulfilled in the future.
|
||||
|
||||
The typical usage is something like:
|
||||
|
||||
```javascript
|
||||
var promise = matrixClient.someMethod(arg1, arg2, callback);
|
||||
```
|
||||
|
||||
The ``callback`` parameter is optional, so you could do:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2).then(function(err, result) {
|
||||
matrixClient.someMethod(arg1, arg2).done(function(result) {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
Alternatively, you could do:
|
||||
Alternatively, if you have a Node.js-style ``callback(err, result)`` function,
|
||||
you can pass the result of the promise into it with something like:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2, function(result) {
|
||||
...
|
||||
});
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
```
|
||||
|
||||
Methods which support this will be clearly marked as returning
|
||||
``Promises``.
|
||||
|
||||
The main thing to note is that it is an error to discard the result of a
|
||||
promise-returning function, as that will cause exceptions to go unobserved. If
|
||||
you have nothing better to do with the result, just call ``.done()`` on it. See
|
||||
http://documentup.com/kriskowal/q/#the-end for more information.
|
||||
|
||||
Methods which return a promise show this in their documentation.
|
||||
|
||||
Many methods in the SDK support *both* Node.js-style callbacks *and* Promises,
|
||||
via an optional ``callback`` argument. The callback support is now deprecated:
|
||||
new methods do not include a ``callback`` argument, and in the future it may be
|
||||
removed from existing methods.
|
||||
|
||||
Examples
|
||||
--------
|
||||
@@ -147,10 +152,10 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
|
||||
### Print out messages for all rooms
|
||||
|
||||
```javascript
|
||||
@@ -166,7 +171,7 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
"(%s) %s :: %s", room.name, event.getSender(), event.getContent().body
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
@@ -198,10 +203,10 @@ Output:
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
|
||||
Output:
|
||||
```
|
||||
My Room
|
||||
@@ -211,7 +216,7 @@ Output:
|
||||
(join) Bob
|
||||
(invite) @charlie:localhost
|
||||
```
|
||||
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
@@ -226,7 +231,7 @@ host the API reference from the source files like this:
|
||||
$ cd .jsdoc
|
||||
$ python -m SimpleHTTPServer 8005
|
||||
```
|
||||
|
||||
|
||||
Then visit ``http://localhost:8005`` to see the API docs.
|
||||
|
||||
Contributing
|
||||
@@ -256,7 +261,7 @@ To run tests (Jasmine)::
|
||||
```
|
||||
$ npm test
|
||||
```
|
||||
|
||||
|
||||
To run linting:
|
||||
```
|
||||
$ npm run lint
|
||||
|
||||
+44
-1
@@ -66,8 +66,9 @@ function MatrixBaseApis(opts) {
|
||||
extraParams: opts.queryParams
|
||||
};
|
||||
this._http = new httpApi.MatrixHttpApi(this, httpOpts);
|
||||
}
|
||||
|
||||
this._txnCtr = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Homeserver URL of this client
|
||||
@@ -100,6 +101,15 @@ MatrixBaseApis.prototype.isLoggedIn = function() {
|
||||
return this._http.opts.accessToken !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Make up a new transaction id
|
||||
*
|
||||
* @return {string} a new, unique, transaction id
|
||||
*/
|
||||
MatrixBaseApis.prototype.makeTxnId = function() {
|
||||
return "m" + new Date().getTime() + "." + (this._txnCtr++);
|
||||
};
|
||||
|
||||
|
||||
// Registration/Login operations
|
||||
// =============================
|
||||
@@ -970,6 +980,39 @@ MatrixBaseApis.prototype.lookupThreePid = function(medium, address, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Direct-to-device messaging
|
||||
// ==========================
|
||||
|
||||
/**
|
||||
* Send an event to a specific list of devices
|
||||
*
|
||||
* @param {string} eventType type of event to send
|
||||
* @param {Object.<string, Object<string, Object>>} contentMap
|
||||
* content to send. Map from user_id to device_id to content object.
|
||||
* @param {string=} txnId transaction id. One will be made up if not
|
||||
* supplied.
|
||||
* @return {module:client.Promise} Resolves to the result object
|
||||
*/
|
||||
MatrixBaseApis.prototype.sendToDevice = function(
|
||||
eventType, contentMap, txnId
|
||||
) {
|
||||
var path = utils.encodeUri("/sendToDevice/$eventType/$txnId", {
|
||||
$eventType: eventType,
|
||||
$txnId: txnId ? txnId : this.makeTxnId(),
|
||||
});
|
||||
|
||||
var body = {
|
||||
messages: contentMap,
|
||||
};
|
||||
|
||||
return this._http.authedRequestWithPrefix(
|
||||
undefined, "PUT", path, undefined, body,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* MatrixBaseApis object
|
||||
*/
|
||||
|
||||
+253
-110
@@ -46,6 +46,7 @@ try {
|
||||
var Crypto = require("./crypto");
|
||||
CRYPTO_ENABLED = true;
|
||||
} catch (e) {
|
||||
console.error("olm load error", e);
|
||||
// Olm not installed.
|
||||
}
|
||||
|
||||
@@ -143,20 +144,20 @@ function MatrixClient(opts) {
|
||||
this._peekSync = null;
|
||||
this._isGuest = false;
|
||||
this._ongoingScrollbacks = {};
|
||||
this._txnCtr = 0;
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
this.urlPreviewCache = {};
|
||||
this._notifTimelineSet = null;
|
||||
|
||||
this._crypto = null;
|
||||
if (CRYPTO_ENABLED && opts.sessionStore !== null &&
|
||||
userId !== null && this.deviceId !== null) {
|
||||
this._crypto = new Crypto(
|
||||
this,
|
||||
this, this,
|
||||
opts.sessionStore,
|
||||
userId, this.deviceId
|
||||
);
|
||||
|
||||
setupCryptoEventHandler(this);
|
||||
this.olmVersion = Crypto.getOlmVersion();
|
||||
}
|
||||
}
|
||||
utils.inherits(MatrixClient, EventEmitter);
|
||||
@@ -249,6 +250,24 @@ MatrixClient.prototype.retryImmediately = function() {
|
||||
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
|
||||
// ===========
|
||||
|
||||
@@ -307,6 +326,8 @@ MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
|
||||
/**
|
||||
* List the stored device keys for a user id
|
||||
*
|
||||
* @deprecated prefer {@link module:client#getStoredDevicesForUser}
|
||||
*
|
||||
* @param {string} userId the user to list keys for.
|
||||
*
|
||||
* @return {object[]} list of devices with "id", "verified", "blocked",
|
||||
@@ -319,6 +340,22 @@ MatrixClient.prototype.listDeviceKeys = function(userId) {
|
||||
return this._crypto.listDeviceKeys(userId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the stored device keys for a user id
|
||||
*
|
||||
* @param {string} userId the user to list keys for.
|
||||
*
|
||||
* @return {module:crypto-deviceinfo[]} list of devices
|
||||
*/
|
||||
MatrixClient.prototype.getStoredDevicesForUser = function(userId) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this._crypto.getStoredDevicesForUser(userId);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Mark the given device as verified
|
||||
*
|
||||
@@ -390,47 +427,6 @@ MatrixClient.prototype.isEventSenderVerified = function(event) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a listener for m.room.encryption events which will enable encryption
|
||||
* for a room.
|
||||
*
|
||||
* @param {MatrixClient} client
|
||||
*/
|
||||
function setupCryptoEventHandler(client) {
|
||||
client.on("event", function(event) {
|
||||
if (!event.isState() || event.getType() != "m.room.encryption") {
|
||||
return;
|
||||
}
|
||||
onCryptoEvent(client, event);
|
||||
});
|
||||
}
|
||||
|
||||
function onCryptoEvent(client, event) {
|
||||
var roomId = event.getRoomId();
|
||||
var content = event.getContent();
|
||||
|
||||
try {
|
||||
client.setRoomEncryption(roomId, content).done();
|
||||
} catch (e) {
|
||||
console.error("Error configuring encryption in room " + roomId +
|
||||
":", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handle a room key event
|
||||
*
|
||||
* @private
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
*/
|
||||
MatrixClient.prototype._onRoomKeyEvent = function(event) {
|
||||
if (!this._crypto) {
|
||||
return;
|
||||
}
|
||||
this._crypto.onRoomKeyEvent(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable end-to-end encryption for a room.
|
||||
* @param {string} roomId The room ID to enable encryption in.
|
||||
@@ -441,18 +437,8 @@ MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
|
||||
if (!this._crypto) {
|
||||
throw new Error("End-to-End encryption disabled");
|
||||
}
|
||||
|
||||
var roomMembers = [];
|
||||
var room = this.getRoom(roomId);
|
||||
if (!room) {
|
||||
console.warn("Enabling encryption in unknown room " + roomId);
|
||||
} else {
|
||||
roomMembers = utils.map(room.getJoinedMembers(), function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
}
|
||||
|
||||
return this._crypto.setRoomEncryption(roomId, config, roomMembers);
|
||||
this._crypto.setRoomEncryption(roomId, config);
|
||||
return q();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -799,7 +785,7 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
|
||||
if (utils.isFunction(txnId)) { callback = txnId; txnId = undefined; }
|
||||
|
||||
if (!txnId) {
|
||||
txnId = "m" + new Date().getTime() + "." + (this._txnCtr++);
|
||||
txnId = this.makeTxnId();
|
||||
}
|
||||
|
||||
// we always construct a MatrixEvent when sending because the store and
|
||||
@@ -900,11 +886,13 @@ function _updatePendingEventStatus(room, event, newStatus) {
|
||||
}
|
||||
|
||||
function _sendEventHttpRequest(client, event) {
|
||||
var txnId = event._txnId ? event._txnId : client.makeTxnId();
|
||||
|
||||
var pathParams = {
|
||||
$roomId: event.getRoomId(),
|
||||
$eventType: event.getWireType(),
|
||||
$stateKey: event.getStateKey(),
|
||||
$txnId: event._txnId ? event._txnId : new Date().getTime()
|
||||
$txnId: txnId,
|
||||
};
|
||||
|
||||
var path;
|
||||
@@ -1361,16 +1349,16 @@ function _membershipChange(client, roomId, userId, membership, reason, callback)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return {module:pushprocessor~PushAction} A dict of actions to perform.
|
||||
*/
|
||||
MatrixClient.prototype.getPushActionsForEvent = function(event) {
|
||||
if (event._pushActions === undefined) {
|
||||
if (!event.getPushActions()) {
|
||||
var pushProcessor = new PushProcessor(this);
|
||||
event._pushActions = pushProcessor.actionsForEvent(event);
|
||||
event.setPushActions(pushProcessor.actionsForEvent(event));
|
||||
}
|
||||
return event._pushActions;
|
||||
return event.getPushActions();
|
||||
};
|
||||
|
||||
// Profile operations
|
||||
@@ -1615,18 +1603,18 @@ MatrixClient.prototype.paginateEventContext = function(eventContext, opts) {
|
||||
/**
|
||||
* 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
|
||||
* 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
|
||||
*
|
||||
* @return {module:client.Promise} Resolves:
|
||||
* {@link module:models/event-timeline~EventTimeline} including the given
|
||||
* event
|
||||
*/
|
||||
MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
||||
MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
|
||||
// don't allow any timeline support unless it's been enabled.
|
||||
if (!this.timelineSupport) {
|
||||
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||
@@ -1634,13 +1622,13 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
||||
" it.");
|
||||
}
|
||||
|
||||
if (room.getTimelineForEvent(eventId)) {
|
||||
return q(room.getTimelineForEvent(eventId));
|
||||
if (timelineSet.getTimelineForEvent(eventId)) {
|
||||
return q(timelineSet.getTimelineForEvent(eventId));
|
||||
}
|
||||
|
||||
var path = utils.encodeUri(
|
||||
"/rooms/$roomId/context/$eventId", {
|
||||
$roomId: room.roomId,
|
||||
$roomId: timelineSet.room.roomId,
|
||||
$eventId: eventId,
|
||||
}
|
||||
);
|
||||
@@ -1657,8 +1645,8 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
||||
|
||||
// by the time the request completes, the event might have ended up in
|
||||
// the timeline.
|
||||
if (room.getTimelineForEvent(eventId)) {
|
||||
return room.getTimelineForEvent(eventId);
|
||||
if (timelineSet.getTimelineForEvent(eventId)) {
|
||||
return timelineSet.getTimelineForEvent(eventId);
|
||||
}
|
||||
|
||||
// we start with the last event, since that's the point at which we
|
||||
@@ -1670,21 +1658,22 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
||||
.concat(res.events_before);
|
||||
var matrixEvents = utils.map(events, self.getEventMapper());
|
||||
|
||||
var timeline = room.getTimelineForEvent(matrixEvents[0].getId());
|
||||
var timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId());
|
||||
if (!timeline) {
|
||||
timeline = room.addTimeline();
|
||||
timeline = timelineSet.addTimeline();
|
||||
timeline.initialiseState(utils.map(res.state,
|
||||
self.getEventMapper()));
|
||||
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
|
||||
// might have switched to a neighbouring timeline) - so check 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
|
||||
// return the timeline we first thought of.
|
||||
return room.getTimelineForEvent(eventId) || timeline;
|
||||
var tl = timelineSet.getTimelineForEvent(eventId) || timeline;
|
||||
return tl;
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
@@ -1696,7 +1685,7 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
||||
* @param {module:models/event-timeline~EventTimeline} eventTimeline timeline
|
||||
* object to be updated
|
||||
* @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
|
||||
* @param {number} [opts.limit = 30] number of events to request
|
||||
*
|
||||
@@ -1704,14 +1693,17 @@ MatrixClient.prototype.getEventTimeline = function(room, eventId) {
|
||||
* events and we reached either end of the timeline; else true.
|
||||
*/
|
||||
MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
|
||||
var isNotifTimeline = (eventTimeline.getTimelineSet() === this._notifTimelineSet);
|
||||
|
||||
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
||||
// nicely with HTTP errors.
|
||||
opts = opts || {};
|
||||
var backwards = opts.backwards || false;
|
||||
|
||||
var room = this.getRoom(eventTimeline.getRoomId());
|
||||
if (!room) {
|
||||
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
||||
if (isNotifTimeline) {
|
||||
if (!backwards) {
|
||||
throw new Error("paginateNotifTimeline can only paginate backwards");
|
||||
}
|
||||
}
|
||||
|
||||
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
@@ -1729,39 +1721,131 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
|
||||
return pendingRequest;
|
||||
}
|
||||
|
||||
var path = utils.encodeUri(
|
||||
"/rooms/$roomId/messages", {$roomId: eventTimeline.getRoomId()}
|
||||
);
|
||||
var params = {
|
||||
from: token,
|
||||
limit: ('limit' in opts) ? opts.limit : 30,
|
||||
dir: dir
|
||||
};
|
||||
|
||||
var path, params, promise;
|
||||
var self = this;
|
||||
|
||||
var promise =
|
||||
this._http.authedRequest(undefined, "GET", path, params
|
||||
).then(function(res) {
|
||||
var token = res.end;
|
||||
var matrixEvents = utils.map(res.chunk, self.getEventMapper());
|
||||
room.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||
if (isNotifTimeline) {
|
||||
path = "/notifications";
|
||||
params = {
|
||||
limit: ('limit' in opts) ? opts.limit : 30,
|
||||
only: 'highlight',
|
||||
};
|
||||
|
||||
// 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.end == res.start) {
|
||||
eventTimeline.setPaginationToken(null, dir);
|
||||
if (token && token !== "end") {
|
||||
params.from = token;
|
||||
}
|
||||
return res.end != res.start;
|
||||
}).finally(function() {
|
||||
eventTimeline._paginationRequests[dir] = null;
|
||||
});
|
||||
eventTimeline._paginationRequests[dir] = promise;
|
||||
|
||||
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()}
|
||||
);
|
||||
params = {
|
||||
from: token,
|
||||
limit: ('limit' in opts) ? opts.limit : 30,
|
||||
dir: dir
|
||||
};
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
promise =
|
||||
this._http.authedRequest(undefined, "GET", path, params
|
||||
).then(function(res) {
|
||||
var token = res.end;
|
||||
var matrixEvents = utils.map(res.chunk, self.getEventMapper());
|
||||
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.end == res.start) {
|
||||
eventTimeline.setPaginationToken(null, dir);
|
||||
}
|
||||
return res.end != res.start;
|
||||
}).finally(function() {
|
||||
eventTimeline._paginationRequests[dir] = null;
|
||||
});
|
||||
eventTimeline._paginationRequests[dir] = 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
|
||||
* history visibility for the room is world_readable.
|
||||
@@ -2273,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
|
||||
* present to a third party in order to prove their ownership
|
||||
@@ -2367,6 +2499,11 @@ MatrixClient.prototype.startClient = function(opts) {
|
||||
|
||||
if (this._crypto) {
|
||||
this._crypto.uploadKeys(5).done();
|
||||
var tenMinutes = 1000 * 60 * 10;
|
||||
var self = this;
|
||||
this._uploadIntervalID = global.setInterval(function() {
|
||||
self._crypto.uploadKeys(5).done();
|
||||
}, tenMinutes);
|
||||
}
|
||||
|
||||
// periodically poll for turn servers if we support voip
|
||||
@@ -2392,6 +2529,9 @@ MatrixClient.prototype.stopClient = function() {
|
||||
this._syncApi.stop();
|
||||
this._syncApi = null;
|
||||
}
|
||||
if (this._crypto) {
|
||||
global.clearInterval(this._uploadIntervalID);
|
||||
}
|
||||
};
|
||||
|
||||
function setupCallEventHandler(client) {
|
||||
@@ -2642,13 +2782,6 @@ function _PojoToMatrixEventMapper(client) {
|
||||
clearData = _decryptMessage(client, plainOldJsObject);
|
||||
}
|
||||
var matrixEvent = new MatrixEvent(plainOldJsObject, clearData);
|
||||
|
||||
// XXXX massive hack to deal with the fact that megolm keys are in the
|
||||
// room for now, and we need to handle them before attempting to
|
||||
// decrypt the following megolm messages.
|
||||
if (matrixEvent.getType() == "m.room_key") {
|
||||
client._onRoomKeyEvent(matrixEvent);
|
||||
}
|
||||
return matrixEvent;
|
||||
}
|
||||
return mapper;
|
||||
@@ -2697,6 +2830,16 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever the SDK receives a new to-device event.
|
||||
* @event module:client~MatrixClient#"toDeviceEvent"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @example
|
||||
* matrixClient.on("toDeviceEvent", function(event){
|
||||
* var sender = event.getSender();
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever the SDK's syncing state is updated. The state can be one of:
|
||||
* <ul>
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 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";
|
||||
|
||||
/**
|
||||
* Defines m.olm encryption/decryption
|
||||
*
|
||||
* @module crypto-algorithms/megolm
|
||||
*/
|
||||
|
||||
var q = require("q");
|
||||
|
||||
var utils = require("../utils");
|
||||
var olmlib = require("../olmlib");
|
||||
var base = require("./base");
|
||||
|
||||
/**
|
||||
* Megolm encryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto-algorithms/base.EncryptionAlgorithm}
|
||||
*
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto-algorithms/base.EncryptionAlgorithm}
|
||||
*/
|
||||
function MegolmEncryption(params) {
|
||||
base.EncryptionAlgorithm.call(this, params);
|
||||
this._prepPromise = null;
|
||||
this._outboundSessionId = null;
|
||||
}
|
||||
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves when setup is
|
||||
* complete.
|
||||
*/
|
||||
MegolmEncryption.prototype._ensureOutboundSession = function(room) {
|
||||
if (this._prepPromise) {
|
||||
// prep already in progress
|
||||
return this._prepPromise;
|
||||
}
|
||||
|
||||
if (this._outboundSessionId) {
|
||||
// prep already done
|
||||
return q();
|
||||
}
|
||||
|
||||
var session_id = this._olmDevice.createOutboundGroupSession();
|
||||
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
|
||||
|
||||
this._olmDevice.addInboundGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
|
||||
key.key, key.chain_index
|
||||
);
|
||||
|
||||
// send the keys to each (unblocked) device in the room.
|
||||
var payload = {
|
||||
type: "m.room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: this._roomId,
|
||||
session_id: session_id,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
}
|
||||
};
|
||||
|
||||
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
var self = this;
|
||||
|
||||
// TODO: we need to give the user a chance to block any devices or users
|
||||
// before we send them the keys; it's too late to download them here.
|
||||
this._prepPromise = this._crypto.downloadKeys(
|
||||
roomMembers, false
|
||||
).then(function(res) {
|
||||
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
|
||||
}).then(function(devicemap) {
|
||||
// TODO: send OOB messages. for now, send an in-band message. Each
|
||||
// encrypted copy of the key takes up about 1K, so we'll only manage
|
||||
// about 60 copies before we hit the event size limit; but ultimately the
|
||||
// OOB messaging API will solve that problem for us.
|
||||
|
||||
var participantKeys = [];
|
||||
for (var userId in devicemap) {
|
||||
if (!devicemap.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var devices = devicemap[userId];
|
||||
|
||||
for (var deviceId in devices) {
|
||||
if (!devices.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var deviceInfo = devices[deviceId].device;
|
||||
participantKeys.push(deviceInfo.getIdentityKey());
|
||||
}
|
||||
}
|
||||
|
||||
var encryptedContent = olmlib.encryptMessageForDevices(
|
||||
self._deviceId,
|
||||
self._olmDevice,
|
||||
participantKeys,
|
||||
payload
|
||||
);
|
||||
|
||||
var txnId = '' + (new Date().getTime());
|
||||
var path = utils.encodeUri(
|
||||
"/rooms/$roomId/send/m.room.encrypted/$txnId", {
|
||||
$roomId: self._roomId,
|
||||
$txnId: txnId,
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: retries
|
||||
return self._baseApis._http.authedRequest(
|
||||
undefined, "PUT", path, undefined, encryptedContent
|
||||
);
|
||||
}).then(function() {
|
||||
// don't set this until the keys are sent successfully; if we get an
|
||||
// error, the user can restart by resending the message.
|
||||
self._outboundSessionId = session_id;
|
||||
}).finally(function() {
|
||||
self._prepPromise = null;
|
||||
});
|
||||
|
||||
return this._prepPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {string} eventType
|
||||
* @param {object} plaintext event content
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
var self = this;
|
||||
return this._ensureOutboundSession(room).then(function() {
|
||||
var payloadJson = {
|
||||
room_id: self._roomId,
|
||||
type: eventType,
|
||||
content: content
|
||||
};
|
||||
|
||||
var ciphertext = self._olmDevice.encryptGroupMessage(
|
||||
self._outboundSessionId, JSON.stringify(payloadJson)
|
||||
);
|
||||
|
||||
var encryptedContent = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||
body: ciphertext,
|
||||
session_id: self._outboundSessionId,
|
||||
signature: "FIXME",
|
||||
};
|
||||
|
||||
return encryptedContent;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Megolm decryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto-algorithms/base.DecryptionAlgorithm}
|
||||
*
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto-algorithms/base.DecryptionAlgorithm}
|
||||
*/
|
||||
function MegolmDecryption(params) {
|
||||
base.DecryptionAlgorithm.call(this, params);
|
||||
}
|
||||
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {object} event raw event
|
||||
*
|
||||
* @return {object} decrypted payload (with properties 'type', 'content')
|
||||
*
|
||||
* @throws {module:crypto-algorithms/base.DecryptionError} if there is a
|
||||
* problem decrypting the event
|
||||
*/
|
||||
MegolmDecryption.prototype.decryptEvent = function(event) {
|
||||
var content = event.content;
|
||||
|
||||
console.log("decrypting " + event.event_id + " with sid " +
|
||||
content.session_id);
|
||||
|
||||
if (!content.sender_key || !content.session_id ||
|
||||
!content.body || !content.signature
|
||||
) {
|
||||
throw new base.DecryptionError("Missing fields in input");
|
||||
}
|
||||
|
||||
try {
|
||||
var res = this._olmDevice.decryptGroupMessage(
|
||||
event.room_id, content.sender_key, content.session_id, content.body
|
||||
);
|
||||
return JSON.parse(res);
|
||||
} catch (e) {
|
||||
throw new base.DecryptionError(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:modules/event~MatrixEvent} event key event
|
||||
*/
|
||||
MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
console.log("Adding key from ", event);
|
||||
var content = event.getContent();
|
||||
|
||||
if (!content.room_id ||
|
||||
!content.session_id ||
|
||||
!content.session_key ||
|
||||
content.chain_index === undefined
|
||||
) {
|
||||
console.error("key event is missing fields");
|
||||
return;
|
||||
}
|
||||
|
||||
this._olmDevice.addInboundGroupSession(
|
||||
content.room_id, event.getSenderKey(), content.session_id,
|
||||
content.session_key, content.chain_index
|
||||
);
|
||||
};
|
||||
|
||||
base.registerAlgorithm(
|
||||
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption
|
||||
);
|
||||
@@ -18,11 +18,11 @@ limitations under the License.
|
||||
/**
|
||||
* olm.js wrapper
|
||||
*
|
||||
* @module OlmDevice
|
||||
* @module crypto/OlmDevice
|
||||
*/
|
||||
|
||||
var Olm = require("olm");
|
||||
var utils = require("./utils");
|
||||
var utils = require("../utils");
|
||||
|
||||
/**
|
||||
* Manages the olm cryptography functions. Each OlmDevice has a single
|
||||
@@ -31,7 +31,7 @@ var utils = require("./utils");
|
||||
* Accounts and sessions are kept pickled in a sessionStore.
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:OlmDevice
|
||||
* @alias module:crypto/OlmDevice
|
||||
*
|
||||
* @param {Object} sessionStore A store to be used for data in end-to-end
|
||||
* crypto
|
||||
@@ -46,14 +46,7 @@ function OlmDevice(sessionStore) {
|
||||
var e2eKeys;
|
||||
var account = new Olm.Account();
|
||||
try {
|
||||
var e2eAccount = this._sessionStore.getEndToEndAccount();
|
||||
if (e2eAccount === null) {
|
||||
account.create();
|
||||
var pickled = account.pickle(this._pickleKey);
|
||||
this._sessionStore.storeEndToEndAccount(pickled);
|
||||
} else {
|
||||
account.unpickle(this._pickleKey, e2eAccount);
|
||||
}
|
||||
_initialise_account(this._sessionStore, this._pickleKey, account);
|
||||
e2eKeys = JSON.parse(account.identity_keys());
|
||||
} finally {
|
||||
account.free();
|
||||
@@ -67,6 +60,25 @@ function OlmDevice(sessionStore) {
|
||||
this._outboundGroupSessionStore = {};
|
||||
}
|
||||
|
||||
function _initialise_account(sessionStore, pickleKey, account) {
|
||||
var e2eAccount = sessionStore.getEndToEndAccount();
|
||||
if (e2eAccount !== null) {
|
||||
account.unpickle(pickleKey, e2eAccount);
|
||||
return;
|
||||
}
|
||||
|
||||
account.create();
|
||||
var pickled = account.pickle(pickleKey);
|
||||
sessionStore.storeEndToEndAccount(pickled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {array} The version of Olm.
|
||||
*/
|
||||
OlmDevice.getOlmVersion = function() {
|
||||
return Olm.get_library_version();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* extract our OlmAccount from the session store and call the given function
|
||||
@@ -311,6 +323,38 @@ OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
|
||||
return sessionIds[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get information on the active Olm sessions for a device.
|
||||
* <p>
|
||||
* Returns an array, with an entry for each active session. The first entry in
|
||||
* the result will be the one used for outgoing messages. Each entry contains
|
||||
* the keys 'hasReceivedMessage' (true if the session has received an incoming
|
||||
* message and is therefore past the pre-key stage), and 'sessionId'.
|
||||
*
|
||||
* @param {string} deviceIdentityKey Curve25519 identity key for the device
|
||||
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
|
||||
*/
|
||||
OlmDevice.prototype.getSessionInfoForDevice = function(deviceIdentityKey) {
|
||||
var sessionIds = this.getSessionIdsForDevice(deviceIdentityKey);
|
||||
sessionIds.sort();
|
||||
|
||||
var info = [];
|
||||
|
||||
function getSessionInfo(session) {
|
||||
return {
|
||||
hasReceivedMessage: session.has_received_message()
|
||||
};
|
||||
}
|
||||
|
||||
for (var i = 0; i < sessionIds.length; i++) {
|
||||
var sessionId = sessionIds[i];
|
||||
var res = this._getSession(deviceIdentityKey, sessionId, getSessionInfo);
|
||||
res.sessionId = sessionId;
|
||||
info.push(res);
|
||||
}
|
||||
return info;
|
||||
};
|
||||
|
||||
/**
|
||||
* Encrypt an outgoing message using an existing session
|
||||
*
|
||||
@@ -478,21 +522,23 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
|
||||
* store an InboundGroupSession in the session store
|
||||
*
|
||||
* @param {string} roomId
|
||||
* @param {string} senderKey
|
||||
* @param {string} senderCurve25519Key
|
||||
* @param {string} sessionId
|
||||
* @param {Olm.InboundGroupSession} session
|
||||
* @param {object} keysClaimed Other keys the sender claims.
|
||||
* @private
|
||||
*/
|
||||
OlmDevice.prototype._saveInboundGroupSession = function(
|
||||
roomId, senderKey, sessionId, session
|
||||
roomId, senderCurve25519Key, sessionId, session, keysClaimed
|
||||
) {
|
||||
var r = {
|
||||
room_id: roomId,
|
||||
session: session.pickle(this._pickleKey),
|
||||
keysClaimed: keysClaimed,
|
||||
};
|
||||
|
||||
this._sessionStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, JSON.stringify(r)
|
||||
senderCurve25519Key, sessionId, JSON.stringify(r)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -503,7 +549,9 @@ OlmDevice.prototype._saveInboundGroupSession = function(
|
||||
* @param {string} senderKey
|
||||
* @param {string} sessionId
|
||||
* @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
|
||||
*/
|
||||
OlmDevice.prototype._getInboundGroupSession = function(
|
||||
@@ -514,7 +562,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
||||
);
|
||||
|
||||
if (r === null) {
|
||||
throw new Error("Unknown inbound group session id");
|
||||
return {sessionExists: false};
|
||||
}
|
||||
|
||||
r = JSON.parse(r);
|
||||
@@ -531,7 +579,12 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
||||
var session = new Olm.InboundGroupSession();
|
||||
try {
|
||||
session.unpickle(this._pickleKey, r.session);
|
||||
return func(session);
|
||||
return {
|
||||
sessionExists: true,
|
||||
result: func(session),
|
||||
keysProved: {curve25519: senderKey},
|
||||
keysClaimed: r.keysClaimed || {},
|
||||
};
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
@@ -552,7 +605,12 @@ OlmDevice.prototype.addInboundGroupSession = function(
|
||||
var self = this;
|
||||
var session = new Olm.InboundGroupSession();
|
||||
try {
|
||||
session.create(chainIndex, sessionKey);
|
||||
session.create(sessionKey);
|
||||
if (sessionId != session.session_id()) {
|
||||
throw new Error(
|
||||
"Mismatched group session ID from senderKey: " + senderKey
|
||||
);
|
||||
}
|
||||
self._saveInboundGroupSession(roomId, senderKey, sessionId, session);
|
||||
} finally {
|
||||
session.free();
|
||||
@@ -567,7 +625,7 @@ OlmDevice.prototype.addInboundGroupSession = function(
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {string} body base64-encoded body of the encrypted message
|
||||
*
|
||||
* @return {string} plaintext
|
||||
* @return {object} {result: "plaintext"|undefined, sessionExists: Boolean}
|
||||
*/
|
||||
OlmDevice.prototype.decryptGroupMessage = function(
|
||||
roomId, senderKey, sessionId, body
|
||||
@@ -18,25 +18,23 @@ limitations under the License.
|
||||
/**
|
||||
* Internal module. Defines the base classes of the encryption implementations
|
||||
*
|
||||
* @module crypto-algorithms/base
|
||||
* @module crypto/algorithms/base
|
||||
*/
|
||||
var q = require("q");
|
||||
|
||||
var utils = require("../utils");
|
||||
var utils = require("../../utils");
|
||||
|
||||
/**
|
||||
* map of registered encryption algorithm classes. A map from string to {@link
|
||||
* module:crypto-algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class
|
||||
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class
|
||||
*
|
||||
* @type {Object.<string, function(new: module:crypto-algorithms/base.EncryptionAlgorithm)>}
|
||||
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
|
||||
*/
|
||||
module.exports.ENCRYPTION_CLASSES = {};
|
||||
|
||||
/**
|
||||
* map of registered encryption algorithm classes. Map from string to {@link
|
||||
* module:crypto-algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class
|
||||
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class
|
||||
*
|
||||
* @type {Object.<string, function(new: module:crypto-algorithms/base.DecryptionAlgorithm)>}
|
||||
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
|
||||
*/
|
||||
module.exports.DECRYPTION_CLASSES = {};
|
||||
|
||||
@@ -44,12 +42,12 @@ module.exports.DECRYPTION_CLASSES = {};
|
||||
* base type for encryption implementations
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:crypto-algorithms/base.EncryptionAlgorithm
|
||||
* @alias module:crypto/algorithms/base.EncryptionAlgorithm
|
||||
*
|
||||
* @param {object} params parameters
|
||||
* @param {string} params.deviceId The identifier for this device.
|
||||
* @param {module:crypto} params.crypto crypto core
|
||||
* @param {module:OlmDevice} params.olmDevice olm.js wrapper
|
||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||
* @param {string} params.roomId The ID of the room we will be sending to
|
||||
*/
|
||||
@@ -63,24 +61,10 @@ var EncryptionAlgorithm = function(params) {
|
||||
/** */
|
||||
module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
|
||||
|
||||
/**
|
||||
* Initialise this EncryptionAlgorithm instance for a particular room.
|
||||
*
|
||||
* <p>This will be called once per EncryptionAlgorithm, just after the
|
||||
* constructor is called.
|
||||
*
|
||||
* @param {string[]} roomMembers list of currently-joined users in the room
|
||||
* @return {module:client.Promise} Promise which resolves when setup is complete
|
||||
*/
|
||||
EncryptionAlgorithm.prototype.initRoomEncryption = function(roomMembers) {
|
||||
return q();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt a message event
|
||||
*
|
||||
* @method module:crypto-algorithms/base.EncryptionAlgorithm#encryptMessage
|
||||
* @method module:crypto/algorithms/base.EncryptionAlgorithm#encryptMessage
|
||||
* @abstract
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
@@ -90,15 +74,34 @@ EncryptionAlgorithm.prototype.initRoomEncryption = function(roomMembers) {
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
|
||||
/**
|
||||
* Called when the membership of a member of the room changes.
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event event causing the change
|
||||
* @param {module:models/room-member} member user whose membership changed
|
||||
* @param {string=} oldMembership previous membership
|
||||
*/
|
||||
EncryptionAlgorithm.prototype.onRoomMembership = function(
|
||||
event, member, oldMembership
|
||||
) {};
|
||||
|
||||
/**
|
||||
* Called when a new device announces itself in the room
|
||||
*
|
||||
* @param {string} userId owner of the device
|
||||
* @param {string} deviceId deviceId of the device
|
||||
*/
|
||||
EncryptionAlgorithm.prototype.onNewDevice = function(userId, deviceId) {};
|
||||
|
||||
|
||||
/**
|
||||
* base type for decryption implementations
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:crypto-algorithms/base.DecryptionAlgorithm
|
||||
* @alias module:crypto/algorithms/base.DecryptionAlgorithm
|
||||
*
|
||||
* @param {object} params parameters
|
||||
* @param {module:OlmDevice} params.olmDevice olm.js wrapper
|
||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||
*/
|
||||
var DecryptionAlgorithm = function(params) {
|
||||
this._olmDevice = params.olmDevice;
|
||||
@@ -109,23 +112,23 @@ module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
|
||||
/**
|
||||
* Decrypt an event
|
||||
*
|
||||
* @method module:crypto-algorithms/base.DecryptionAlgorithm#decryptEvent
|
||||
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
|
||||
* @abstract
|
||||
*
|
||||
* @param {object} event raw event
|
||||
*
|
||||
* @return {object} decrypted payload (with properties 'type', 'content')
|
||||
*
|
||||
* @throws {module:crypto-algorithms/base.DecryptionError} if there is a
|
||||
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
||||
* problem decrypting the event
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handle a key event
|
||||
*
|
||||
* @method module:crypto-algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
|
||||
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
|
||||
*
|
||||
* @param {module:modules/event~MatrixEvent} event key event
|
||||
* @param {module:models/event.MatrixEvent} event key event
|
||||
*/
|
||||
DecryptionAlgorithm.prototype.onRoomKeyEvent = function(params) {
|
||||
// ignore by default
|
||||
@@ -149,11 +152,11 @@ utils.inherits(module.exports.DecryptionError, Error);
|
||||
* @param {string} algorithm algorithm tag to register for
|
||||
*
|
||||
* @param {class} encryptor {@link
|
||||
* module:crypto-algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm}
|
||||
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm}
|
||||
* implementation
|
||||
*
|
||||
* @param {class} decryptor {@link
|
||||
* module:crypto-algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
|
||||
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
|
||||
* implementation
|
||||
*/
|
||||
module.exports.registerAlgorithm = function(algorithm, encryptor, decryptor) {
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @module crypto-algorithms
|
||||
* @module crypto/algorithms
|
||||
*/
|
||||
|
||||
var base = require("./base");
|
||||
@@ -25,16 +25,16 @@ require("./olm");
|
||||
require("./megolm");
|
||||
|
||||
/**
|
||||
* @see module:crypto-algorithms/base.ENCRYPTION_CLASSES
|
||||
* @see module:crypto/algorithms/base.ENCRYPTION_CLASSES
|
||||
*/
|
||||
module.exports.ENCRYPTION_CLASSES = base.ENCRYPTION_CLASSES;
|
||||
|
||||
/**
|
||||
* @see module:crypto-algorithms/base.DECRYPTION_CLASSES
|
||||
* @see module:crypto/algorithms/base.DECRYPTION_CLASSES
|
||||
*/
|
||||
module.exports.DECRYPTION_CLASSES = base.DECRYPTION_CLASSES;
|
||||
|
||||
/**
|
||||
* @see module:crypto-algorithms/base.DecryptionError
|
||||
* @see module:crypto/algorithms/base.DecryptionError
|
||||
*/
|
||||
module.exports.DecryptionError = base.DecryptionError;
|
||||
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
Copyright 2015, 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";
|
||||
|
||||
/**
|
||||
* Defines m.olm encryption/decryption
|
||||
*
|
||||
* @module crypto/algorithms/megolm
|
||||
*/
|
||||
|
||||
var q = require("q");
|
||||
|
||||
var utils = require("../../utils");
|
||||
var olmlib = require("../olmlib");
|
||||
var base = require("./base");
|
||||
|
||||
/**
|
||||
* Megolm encryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
|
||||
*
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
|
||||
*/
|
||||
function MegolmEncryption(params) {
|
||||
base.EncryptionAlgorithm.call(this, params);
|
||||
this._prepPromise = null;
|
||||
this._outboundSessionId = null;
|
||||
this._discardNewSession = false;
|
||||
|
||||
// devices which have joined since we last sent a message.
|
||||
// userId -> {deviceId -> true}, or
|
||||
// userId -> true
|
||||
this._devicesPendingKeyShare = {};
|
||||
this._sharePromise = null;
|
||||
}
|
||||
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the megolm
|
||||
* sessionId when setup is complete.
|
||||
*/
|
||||
MegolmEncryption.prototype._ensureOutboundSession = function(room) {
|
||||
var self = this;
|
||||
|
||||
if (this._prepPromise) {
|
||||
// prep already in progress
|
||||
return this._prepPromise;
|
||||
}
|
||||
|
||||
var sessionId = this._outboundSessionId;
|
||||
|
||||
// need to make a brand new session?
|
||||
if (!sessionId) {
|
||||
this._prepPromise = this._prepareNewSession(room).
|
||||
finally(function() {
|
||||
self._prepPromise = null;
|
||||
});
|
||||
return this._prepPromise;
|
||||
}
|
||||
|
||||
if (this._sharePromise) {
|
||||
// key share already in progress
|
||||
return this._sharePromise;
|
||||
}
|
||||
|
||||
// prep already done, but check for new devices
|
||||
var shareMap = this._devicesPendingKeyShare;
|
||||
this._devicesPendingKeyShare = {};
|
||||
|
||||
// check each user is (still) a member of the room
|
||||
for (var userId in shareMap) {
|
||||
if (!shareMap.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// XXX what about rooms where invitees can see the content?
|
||||
var member = room.getMember(userId);
|
||||
if (member.membership !== "join") {
|
||||
delete shareMap[userId];
|
||||
}
|
||||
}
|
||||
|
||||
this._sharePromise = this._shareKeyWithDevices(
|
||||
sessionId, shareMap
|
||||
).finally(function() {
|
||||
self._sharePromise = null;
|
||||
}).then(function() {
|
||||
return sessionId;
|
||||
});
|
||||
|
||||
return this._sharePromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the megolm
|
||||
* sessionId when setup is complete.
|
||||
*/
|
||||
MegolmEncryption.prototype._prepareNewSession = function(room) {
|
||||
var session_id = this._olmDevice.createOutboundGroupSession();
|
||||
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
|
||||
|
||||
this._olmDevice.addInboundGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
|
||||
key.key
|
||||
);
|
||||
|
||||
// we're going to share the key with all current members of the room,
|
||||
// so we can reset this.
|
||||
this._devicesPendingKeyShare = {};
|
||||
|
||||
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
var shareMap = {};
|
||||
for (var i = 0; i < roomMembers.length; i++) {
|
||||
var userId = roomMembers[i];
|
||||
shareMap[userId] = true;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
// TODO: we need to give the user a chance to block any devices or users
|
||||
// before we send them the keys; it's too late to download them here.
|
||||
return this._crypto.downloadKeys(
|
||||
roomMembers, false
|
||||
).then(function(res) {
|
||||
return self._shareKeyWithDevices(session_id, shareMap);
|
||||
}).then(function() {
|
||||
if (self._discardNewSession) {
|
||||
// we've had cause to reset the session_id since starting this process.
|
||||
// we'll use the current session for any currently pending events, but
|
||||
// don't save it as the current _outboundSessionId, so that new events
|
||||
// will use a new session.
|
||||
console.log("Session generation complete, but discarding");
|
||||
} else {
|
||||
self._outboundSessionId = session_id;
|
||||
}
|
||||
return session_id;
|
||||
}).finally(function() {
|
||||
self._discardNewSession = false;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {string} session_id
|
||||
* @param {Object<string, Object<string, boolean>|boolean>} shareMap
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves once the key sharing
|
||||
* message has been sent.
|
||||
*/
|
||||
MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap) {
|
||||
var self = this;
|
||||
|
||||
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
|
||||
var payload = {
|
||||
type: "m.room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: this._roomId,
|
||||
session_id: session_id,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
}
|
||||
};
|
||||
|
||||
return self._crypto.ensureOlmSessionsForUsers(
|
||||
utils.keys(shareMap)
|
||||
).then(function(devicemap) {
|
||||
var contentMap = {};
|
||||
var haveTargets = false;
|
||||
|
||||
for (var userId in devicemap) {
|
||||
if (!devicemap.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var devicesToShareWith = shareMap[userId];
|
||||
var deviceInfos = devicemap[userId];
|
||||
|
||||
for (var deviceId in deviceInfos) {
|
||||
if (!deviceInfos.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (devicesToShareWith === true) {
|
||||
// all devices
|
||||
} else if (!devicesToShareWith[deviceId]) {
|
||||
// not a new device
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"sharing keys with device " + userId + ":" + deviceId
|
||||
);
|
||||
|
||||
var deviceInfo = deviceInfos[deviceId].device;
|
||||
|
||||
if (!contentMap[userId]) {
|
||||
contentMap[userId] = {};
|
||||
}
|
||||
|
||||
contentMap[userId][deviceId] =
|
||||
olmlib.encryptMessageForDevices(
|
||||
self._deviceId,
|
||||
self._olmDevice,
|
||||
[deviceInfo.getIdentityKey()],
|
||||
payload
|
||||
);
|
||||
haveTargets = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!haveTargets) {
|
||||
return q();
|
||||
}
|
||||
|
||||
// TODO: retries
|
||||
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {string} eventType
|
||||
* @param {object} plaintext event content
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
var self = this;
|
||||
return this._ensureOutboundSession(room).then(function(session_id) {
|
||||
var payloadJson = {
|
||||
room_id: self._roomId,
|
||||
type: eventType,
|
||||
content: content
|
||||
};
|
||||
|
||||
var ciphertext = self._olmDevice.encryptGroupMessage(
|
||||
session_id, JSON.stringify(payloadJson)
|
||||
);
|
||||
|
||||
var encryptedContent = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: ciphertext,
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event event causing the change
|
||||
* @param {module:models/room-member} member user whose membership changed
|
||||
* @param {string=} oldMembership previous membership
|
||||
*/
|
||||
MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) {
|
||||
var newMembership = member.membership;
|
||||
|
||||
if (newMembership === 'join') {
|
||||
// new member in the room.
|
||||
this._devicesPendingKeyShare[member.userId] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMembership === 'invite' && oldMembership !== 'join') {
|
||||
// we don't (yet) share keys with invited members, so nothing to do yet
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise we assume the user is leaving, and start a new outbound session.
|
||||
if (this._outboundSessionId) {
|
||||
console.log("Discarding outbound megolm session due to change in " +
|
||||
"membership of " + member.userId + " (" + oldMembership +
|
||||
"->" + newMembership + ")");
|
||||
this._outboundSessionId = null;
|
||||
}
|
||||
|
||||
if (this._prepPromise) {
|
||||
console.log("Discarding as-yet-incomplete megolm session due to " +
|
||||
"change in membership of " + member.userId + " (" +
|
||||
oldMembership + "->" + newMembership + ")");
|
||||
this._discardNewSession = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {string} userId owner of the device
|
||||
* @param {string} deviceId deviceId of the device
|
||||
*/
|
||||
MegolmEncryption.prototype.onNewDevice = function(userId, deviceId) {
|
||||
var d = this._devicesPendingKeyShare[userId];
|
||||
|
||||
if (d === true) {
|
||||
// we already want to share keys with all devices for this user
|
||||
return;
|
||||
}
|
||||
|
||||
if (!d) {
|
||||
this._devicesPendingKeyShare[userId] = d = {};
|
||||
}
|
||||
|
||||
d[deviceId] = true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Megolm decryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
|
||||
*
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
|
||||
*/
|
||||
function MegolmDecryption(params) {
|
||||
base.DecryptionAlgorithm.call(this, params);
|
||||
}
|
||||
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {object} event raw event
|
||||
*
|
||||
* @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
|
||||
* problem decrypting the event
|
||||
*/
|
||||
MegolmDecryption.prototype.decryptEvent = function(event) {
|
||||
var content = event.content;
|
||||
|
||||
console.log("decrypting " + event.event_id + " with sid " +
|
||||
content.session_id);
|
||||
|
||||
if (!content.sender_key || !content.session_id ||
|
||||
!content.ciphertext
|
||||
) {
|
||||
throw new base.DecryptionError("Missing fields in input");
|
||||
}
|
||||
|
||||
try {
|
||||
var res = this._olmDevice.decryptGroupMessage(
|
||||
event.room_id, content.sender_key, content.session_id, content.ciphertext
|
||||
);
|
||||
if (res.sessionExists) {
|
||||
res.result = JSON.parse(res.result);
|
||||
return res;
|
||||
} else {
|
||||
return {sessionExists: false};
|
||||
}
|
||||
} catch (e) {
|
||||
throw new base.DecryptionError(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event key event
|
||||
*/
|
||||
MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
console.log("Adding key from ", event);
|
||||
var content = event.getContent();
|
||||
|
||||
if (!content.room_id ||
|
||||
!content.session_id ||
|
||||
!content.session_key
|
||||
) {
|
||||
console.error("key event is missing fields");
|
||||
return;
|
||||
}
|
||||
|
||||
this._olmDevice.addInboundGroupSession(
|
||||
content.room_id, event.getSenderKey(), content.session_id,
|
||||
content.session_key, event.getKeysClaimed()
|
||||
);
|
||||
};
|
||||
|
||||
base.registerAlgorithm(
|
||||
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption
|
||||
);
|
||||
@@ -18,13 +18,13 @@ limitations under the License.
|
||||
/**
|
||||
* Defines m.olm encryption/decryption
|
||||
*
|
||||
* @module crypto-algorithms/olm
|
||||
* @module crypto/algorithms/olm
|
||||
*/
|
||||
var q = require('q');
|
||||
|
||||
var utils = require("../utils");
|
||||
var utils = require("../../utils");
|
||||
var olmlib = require("../olmlib");
|
||||
var DeviceInfo = require("../crypto-deviceinfo");
|
||||
var DeviceInfo = require("../deviceinfo");
|
||||
var DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
|
||||
@@ -34,26 +34,44 @@ var base = require("./base");
|
||||
* Olm encryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto-algorithms/base.EncryptionAlgorithm}
|
||||
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
|
||||
*
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto-algorithms/base.EncryptionAlgorithm}
|
||||
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
|
||||
*/
|
||||
function OlmEncryption(params) {
|
||||
base.EncryptionAlgorithm.call(this, params);
|
||||
this._sessionPrepared = false;
|
||||
this._prepPromise = null;
|
||||
}
|
||||
utils.inherits(OlmEncryption, base.EncryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @private
|
||||
|
||||
* @param {string[]} roomMembers list of currently-joined users in the room
|
||||
* @return {module:client.Promise} Promise which resolves when setup is complete
|
||||
*/
|
||||
OlmEncryption.prototype.initRoomEncryption = function(roomMembers) {
|
||||
var crypto = this._crypto;
|
||||
return crypto.downloadKeys(roomMembers, true).then(function(res) {
|
||||
return crypto.ensureOlmSessionsForUsers(roomMembers);
|
||||
OlmEncryption.prototype._ensureSession = function(roomMembers) {
|
||||
if (this._prepPromise) {
|
||||
// prep already in progress
|
||||
return this._prepPromise;
|
||||
}
|
||||
|
||||
if (this._sessionPrepared) {
|
||||
// prep already done
|
||||
return q();
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this._prepPromise = self._crypto.downloadKeys(roomMembers, true).then(function(res) {
|
||||
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
|
||||
}).then(function() {
|
||||
self._sessionPrepared = true;
|
||||
}).finally(function() {
|
||||
self._prepPromise = null;
|
||||
});
|
||||
return this._prepPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -75,44 +93,55 @@ OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
var participantKeys = [];
|
||||
for (var i = 0; i < users.length; ++i) {
|
||||
var userId = users[i];
|
||||
var devices = this._crypto.getStoredDevicesForUser(userId);
|
||||
for (var j = 0; j < devices.length; ++j) {
|
||||
var deviceInfo = devices[j];
|
||||
var key = deviceInfo.getIdentityKey();
|
||||
if (key == this._olmDevice.deviceCurve25519Key) {
|
||||
// don't bother setting up session to ourself
|
||||
continue;
|
||||
var self = this;
|
||||
return this._ensureSession(users).then(function() {
|
||||
var participantKeys = [];
|
||||
for (var i = 0; i < users.length; ++i) {
|
||||
var userId = users[i];
|
||||
var devices = self._crypto.getStoredDevicesForUser(userId);
|
||||
for (var j = 0; j < devices.length; ++j) {
|
||||
var deviceInfo = devices[j];
|
||||
var key = deviceInfo.getIdentityKey();
|
||||
if (key == self._olmDevice.deviceCurve25519Key) {
|
||||
// don't bother setting up session to ourself
|
||||
continue;
|
||||
}
|
||||
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
|
||||
// don't bother setting up sessions with blocked users
|
||||
continue;
|
||||
}
|
||||
participantKeys.push(key);
|
||||
}
|
||||
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
|
||||
// don't bother setting up sessions with blocked users
|
||||
continue;
|
||||
}
|
||||
participantKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return q(
|
||||
olmlib.encryptMessageForDevices(
|
||||
this._deviceId, this._olmDevice, participantKeys, {
|
||||
return olmlib.encryptMessageForDevices(
|
||||
self._deviceId, self._olmDevice, participantKeys, {
|
||||
room_id: room.roomId,
|
||||
type: eventType,
|
||||
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
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Olm decryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto-algorithms/base.DecryptionAlgorithm}
|
||||
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto-algorithms/base.DecryptionAlgorithm}
|
||||
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
|
||||
*/
|
||||
function OlmDecryption(params) {
|
||||
base.DecryptionAlgorithm.call(this, params);
|
||||
@@ -124,9 +153,11 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
OlmDecryption.prototype.decryptEvent = function(event) {
|
||||
@@ -173,13 +204,20 @@ OlmDecryption.prototype.decryptEvent = function(event) {
|
||||
console.log("created new inbound sesion");
|
||||
} catch (e) {
|
||||
// Failed to decrypt with a new session.
|
||||
console.warn("Failed to decrypt message with new session", e);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Check the sender user id matches the sender key.
|
||||
// TODO: check the room_id and fingerprint
|
||||
if (payloadString !== null) {
|
||||
return JSON.parse(payloadString);
|
||||
var payload = JSON.parse(payloadString);
|
||||
return {
|
||||
result: payload,
|
||||
sessionExists: true,
|
||||
keysProved: {curve25519: deviceKey},
|
||||
keysClaimed: payload.keys || {}
|
||||
};
|
||||
} else {
|
||||
throw new base.DecryptionError("Bad Encrypted Message");
|
||||
}
|
||||
@@ -17,14 +17,14 @@ limitations under the License.
|
||||
|
||||
|
||||
/**
|
||||
* @module crypto-deviceinfo
|
||||
* @module crypto/deviceinfo
|
||||
*/
|
||||
|
||||
/**
|
||||
* Information about a user's device
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:crypto-deviceinfo
|
||||
* @alias module:crypto/deviceinfo
|
||||
*
|
||||
* @property {string} deviceId the ID of this device
|
||||
*
|
||||
@@ -33,7 +33,7 @@ limitations under the License.
|
||||
* @property {Object.<string,string>} keys a map from
|
||||
* <key type>:<id> -> <base64-encoded key>>
|
||||
*
|
||||
* @property {module:crypto-deviceinfo.DeviceVerification} verified
|
||||
* @property {module:crypto/deviceinfo.DeviceVerification} verified
|
||||
* whether the device has been verified by the user
|
||||
*
|
||||
* @property {Object} unsigned additional data from the homeserver
|
||||
@@ -108,10 +108,28 @@ DeviceInfo.prototype.getIdentityKey = function() {
|
||||
*
|
||||
* @return {string?} displayname
|
||||
*/
|
||||
DeviceInfo.prototype.getDisplayname = function() {
|
||||
DeviceInfo.prototype.getDisplayName = function() {
|
||||
return this.unsigned.device_display_name || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if this device is blocked
|
||||
*
|
||||
* @return {Boolean} true if blocked
|
||||
*/
|
||||
DeviceInfo.prototype.isBlocked = function() {
|
||||
return this.verified == DeviceVerification.BLOCKED;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if this device is verified
|
||||
*
|
||||
* @return {Boolean} true if verified
|
||||
*/
|
||||
DeviceInfo.prototype.isVerified = function() {
|
||||
return this.verified == DeviceVerification.VERIFIED;
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum
|
||||
*/
|
||||
@@ -23,23 +23,24 @@ limitations under the License.
|
||||
var anotherjson = require('another-json');
|
||||
var q = require("q");
|
||||
|
||||
var utils = require("../utils");
|
||||
var OlmDevice = require("./OlmDevice");
|
||||
var olmlib = require("./olmlib");
|
||||
|
||||
var algorithms = require("./crypto-algorithms");
|
||||
|
||||
var DeviceInfo = require("./crypto-deviceinfo");
|
||||
var algorithms = require("./algorithms");
|
||||
var DeviceInfo = require("./deviceinfo");
|
||||
var DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
/**
|
||||
* Cryptography bits
|
||||
*
|
||||
* @alias module:crypto.Crypto
|
||||
* @constructor
|
||||
* @alias module:crypto
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||
*
|
||||
* @param {external:EventEmitter} eventEmitter event source where we can register
|
||||
* for event notifications
|
||||
*
|
||||
* @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore
|
||||
* Store to be used for end-to-end crypto session data
|
||||
*
|
||||
@@ -47,7 +48,7 @@ var DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
*
|
||||
* @param {string} deviceId The identifier for this device.
|
||||
*/
|
||||
function Crypto(baseApis, sessionStore, userId, deviceId) {
|
||||
function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
|
||||
this._baseApis = baseApis;
|
||||
this._sessionStore = sessionStore;
|
||||
this._userId = userId;
|
||||
@@ -58,6 +59,10 @@ function Crypto(baseApis, sessionStore, userId, deviceId) {
|
||||
// EncryptionAlgorithm instance for each room
|
||||
this._roomAlgorithms = {};
|
||||
|
||||
this._supportedAlgorithms = utils.keys(
|
||||
algorithms.DECRYPTION_CLASSES
|
||||
);
|
||||
|
||||
// build our device keys: these will later be uploaded
|
||||
this._deviceKeys = {};
|
||||
this._deviceKeys["ed25519:" + this._deviceId] =
|
||||
@@ -68,7 +73,7 @@ function Crypto(baseApis, sessionStore, userId, deviceId) {
|
||||
// add our own deviceinfo to the sessionstore
|
||||
var deviceInfo = {
|
||||
keys: this._deviceKeys,
|
||||
algorithms: [olmlib.OLM_ALGORITHM],
|
||||
algorithms: this._supportedAlgorithms,
|
||||
verified: DeviceVerification.VERIFIED,
|
||||
};
|
||||
var myDevices = this._sessionStore.getEndToEndDevicesForUser(
|
||||
@@ -78,8 +83,65 @@ function Crypto(baseApis, sessionStore, userId, deviceId) {
|
||||
this._sessionStore.storeEndToEndDevicesForUser(
|
||||
this._userId, myDevices
|
||||
);
|
||||
|
||||
_registerEventHandlers(this, eventEmitter);
|
||||
|
||||
this._lastNewDeviceMessageTsByUserDeviceRoom = {};
|
||||
}
|
||||
|
||||
function _registerEventHandlers(crypto, eventEmitter) {
|
||||
eventEmitter.on("sync", function(syncState, oldState, data) {
|
||||
try {
|
||||
if (syncState == "PREPARED") {
|
||||
// XXX ugh. we're assuming the eventEmitter is a MatrixClient.
|
||||
// how can we avoid doing so?
|
||||
var rooms = eventEmitter.getRooms();
|
||||
crypto._onInitialSyncCompleted(rooms);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error handling sync", e);
|
||||
}
|
||||
});
|
||||
|
||||
eventEmitter.on("RoomMember.membership", function(event, member, oldMembership) {
|
||||
try {
|
||||
crypto._onRoomMembership(event, member, oldMembership);
|
||||
} catch (e) {
|
||||
console.error("Error handling membership change:", e);
|
||||
}
|
||||
});
|
||||
|
||||
eventEmitter.on("toDeviceEvent", function(event) {
|
||||
try {
|
||||
if (event.getType() == "m.room_key") {
|
||||
crypto._onRoomKeyEvent(event);
|
||||
} else if (event.getType() == "m.new_device") {
|
||||
crypto._onNewDeviceEvent(event);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error handling toDeviceEvent:", e);
|
||||
}
|
||||
});
|
||||
|
||||
eventEmitter.on("event", function(event) {
|
||||
try {
|
||||
if (!event.isState() || event.getType() != "m.room.encryption") {
|
||||
return;
|
||||
}
|
||||
crypto._onCryptoEvent(event);
|
||||
} catch (e) {
|
||||
console.error("Error handling crypto event:", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string} The version of Olm.
|
||||
*/
|
||||
Crypto.getOlmVersion = function() {
|
||||
return OlmDevice.getOlmVersion();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the Ed25519 key for this device
|
||||
*
|
||||
@@ -98,18 +160,46 @@ Crypto.prototype.getDeviceEd25519Key = function() {
|
||||
Crypto.prototype.uploadKeys = function(maxKeys) {
|
||||
var self = this;
|
||||
return _uploadDeviceKeys(this).then(function(res) {
|
||||
// We need to keep a pool of one time public keys on the server so that
|
||||
// other devices can start conversations with us. But we can only store
|
||||
// a finite number of private keys in the olm Account object.
|
||||
// To complicate things further then can be a delay between a device
|
||||
// claiming a public one time key from the server and it sending us a
|
||||
// message. We need to keep the corresponding private key locally until
|
||||
// we receive the message.
|
||||
// But that message might never arrive leaving us stuck with duff
|
||||
// private keys clogging up our local storage.
|
||||
// So we need some kind of enginering compromise to balance all of
|
||||
// these factors.
|
||||
|
||||
// We first find how many keys the server has for us.
|
||||
var keyCount = res.one_time_key_counts.curve25519 || 0;
|
||||
// We then check how many keys we can store in the Account object.
|
||||
var maxOneTimeKeys = self._olmDevice.maxNumberOfOneTimeKeys();
|
||||
// Try to keep at most half that number on the server. This leaves the
|
||||
// rest of the slots free to hold keys that have been claimed from the
|
||||
// server but we haven't recevied a message for.
|
||||
// If we run out of slots when generating new keys then olm will
|
||||
// discard the oldest private keys first. This will eventually clean
|
||||
// out stale private keys that won't receive a message.
|
||||
var keyLimit = Math.floor(maxOneTimeKeys / 2);
|
||||
// We work out how many new keys we need to create to top up the server
|
||||
// If there are too many keys on the server then we don't need to
|
||||
// create any more keys.
|
||||
var numberToGenerate = Math.max(keyLimit - keyCount, 0);
|
||||
if (maxKeys !== undefined) {
|
||||
// Creating keys can be an expensive operation so we limit the
|
||||
// number we generate in one go to avoid blocking the application
|
||||
// for too long.
|
||||
numberToGenerate = Math.min(numberToGenerate, maxKeys);
|
||||
}
|
||||
|
||||
if (numberToGenerate <= 0) {
|
||||
// If we don't need to generate any keys then we are done.
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask olm to generate new one time keys, then upload them to synapse.
|
||||
self._olmDevice.generateOneTimeKeys(numberToGenerate);
|
||||
return _uploadOneTimeKeys(self);
|
||||
});
|
||||
@@ -121,7 +211,7 @@ function _uploadDeviceKeys(crypto) {
|
||||
var deviceId = crypto._deviceId;
|
||||
|
||||
var deviceKeys = {
|
||||
algorithms: [olmlib.OLM_ALGORITHM],
|
||||
algorithms: crypto._supportedAlgorithms,
|
||||
device_id: deviceId,
|
||||
keys: crypto._deviceKeys,
|
||||
user_id: userId,
|
||||
@@ -170,7 +260,7 @@ function _uploadOneTimeKeys(crypto) {
|
||||
* @param {bool} forceDownload Always download the keys even if cached.
|
||||
*
|
||||
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
|
||||
* module:crypto-deviceinfo|DeviceInfo}.
|
||||
* module:crypto/deviceinfo|DeviceInfo}.
|
||||
*/
|
||||
Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
|
||||
var self = this;
|
||||
@@ -288,7 +378,7 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
|
||||
return false;
|
||||
}
|
||||
|
||||
var unsigned = deviceResult.unsigned;
|
||||
var unsigned = deviceResult.unsigned || {};
|
||||
var signatures = deviceResult.signatures || {};
|
||||
var userSigs = signatures[userId] || {};
|
||||
var signature = userSigs[signKeyId];
|
||||
@@ -332,19 +422,18 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
|
||||
userStore[deviceId] = deviceStore = new DeviceInfo(deviceId);
|
||||
}
|
||||
|
||||
deviceStore.keys = deviceResult.keys;
|
||||
deviceStore.algorithms = deviceResult.algorithms;
|
||||
deviceStore.keys = deviceResult.keys || {};
|
||||
deviceStore.algorithms = deviceResult.algorithms || [];
|
||||
deviceStore.unsigned = unsigned;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the stored device keys for a user id
|
||||
*
|
||||
* @param {string} userId the user to list keys for.
|
||||
*
|
||||
* @return {module:crypto-deviceinfo[]} list of devices
|
||||
* @return {module:crypto/deviceinfo[]} list of devices
|
||||
*/
|
||||
Crypto.prototype.getStoredDevicesForUser = function(userId) {
|
||||
var devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
@@ -383,9 +472,9 @@ Crypto.prototype.listDeviceKeys = function(userId) {
|
||||
result.push({
|
||||
id: device.deviceId,
|
||||
key: ed25519Key,
|
||||
verified: Boolean(device.verified == DeviceVerification.VERIFIED),
|
||||
blocked: Boolean(device.verified == DeviceVerification.BLOCKED),
|
||||
display_name: device.getDisplayname(),
|
||||
verified: Boolean(device.isVerified()),
|
||||
blocked: Boolean(device.isBlocked()),
|
||||
display_name: device.getDisplayName(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -407,10 +496,13 @@ Crypto.prototype.listDeviceKeys = function(userId) {
|
||||
* @param {string} algorithm encryption algorithm
|
||||
* @param {string} sender_key curve25519 key to match
|
||||
*
|
||||
* @return {module:crypto-deviceinfo?}
|
||||
* @return {module:crypto/deviceinfo?}
|
||||
*/
|
||||
Crypto.prototype.getDeviceByIdentityKey = function(userId, algorithm, sender_key) {
|
||||
if (algorithm !== olmlib.OLM_ALGORITHM) {
|
||||
if (
|
||||
algorithm !== olmlib.OLM_ALGORITHM &&
|
||||
algorithm !== olmlib.MEGOLM_ALGORITHM
|
||||
) {
|
||||
// we only deal in olm keys
|
||||
return null;
|
||||
}
|
||||
@@ -486,6 +578,37 @@ Crypto.prototype.setDeviceVerification = function(userId, deviceId, verified, bl
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get information on the active olm sessions with a user
|
||||
* <p>
|
||||
* Returns a map from device id to an object with keys 'deviceIdKey' (the
|
||||
* device's curve25519 identity key) and 'sessions' (an array of objects in the
|
||||
* same format as that returned by
|
||||
* {@link module:crypto/OlmDevice#getSessionInfoForDevice}).
|
||||
* <p>
|
||||
* This method is provided for debugging purposes.
|
||||
*
|
||||
* @param {string} userId id of user to inspect
|
||||
*
|
||||
* @return {Object.<string, {deviceIdKey: string, sessions: object[]}>}
|
||||
*/
|
||||
Crypto.prototype.getOlmSessionsForUser = function(userId) {
|
||||
var devices = this.getStoredDevicesForUser(userId);
|
||||
var result = {};
|
||||
for (var j = 0; j < devices.length; ++j) {
|
||||
var device = devices[j];
|
||||
var deviceKey = device.getIdentityKey();
|
||||
var sessions = this._olmDevice.getSessionInfoForDevice(deviceKey);
|
||||
|
||||
result[device.deviceId] = {
|
||||
deviceIdKey: deviceKey,
|
||||
sessions: sessions,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Identify a device by curve25519 identity key and determine its verification state
|
||||
*
|
||||
@@ -496,6 +619,19 @@ Crypto.prototype.setDeviceVerification = function(userId, deviceId, verified, bl
|
||||
* @return {boolean} true if the device is verified
|
||||
*/
|
||||
Crypto.prototype.isSenderKeyVerified = function(userId, algorithm, sender_key) {
|
||||
|
||||
// sender_key is the curve25519 public key of the device, that the event
|
||||
// purports to have been sent from. It's assumed that, by the time we get here,
|
||||
// we have already checked that the event was, in fact, sent by that device.
|
||||
//
|
||||
// In the case of both olm and megolm, that is achieved primarily by the
|
||||
// fact that sessions are indexed by the curve25519 key of the device that
|
||||
// created the session, and we assume that only that device has the keys
|
||||
// necessary to create valid messages in that session.
|
||||
//
|
||||
// So, all we need to do here is look up the device by sender and
|
||||
// curve25519 key and determine the state of the verification flag.
|
||||
|
||||
var device = this.getDeviceByIdentityKey(userId, algorithm, sender_key);
|
||||
if (!device) {
|
||||
return false;
|
||||
@@ -509,11 +645,8 @@ Crypto.prototype.isSenderKeyVerified = function(userId, algorithm, sender_key) {
|
||||
*
|
||||
* @param {string} roomId The room ID to enable encryption in.
|
||||
* @param {object} config The encryption config for the room.
|
||||
* @param {string[]} roomMembers userIds of room members to start sessions with
|
||||
*
|
||||
* @return {module:client.Promise} A promise that will resolve when encryption is setup.
|
||||
*/
|
||||
Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
|
||||
Crypto.prototype.setRoomEncryption = function(roomId, config) {
|
||||
// if we already have encryption in this room, we should ignore this event
|
||||
// (for now at least. maybe we should alert the user somehow?)
|
||||
var existingConfig = this._sessionStore.getEndToEndRoom(roomId);
|
||||
@@ -521,7 +654,7 @@ Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
|
||||
if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
|
||||
console.error("Ignoring m.room.encryption event which requests " +
|
||||
"a change of config in " + roomId);
|
||||
return q();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,13 +677,12 @@ Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
|
||||
roomId: roomId,
|
||||
});
|
||||
this._roomAlgorithms[roomId] = alg;
|
||||
return alg.initRoomEncryption(roomMembers);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {Object} module:crypto~OlmSessionResult
|
||||
* @property {module:crypto-deviceinfo} device device info
|
||||
* @property {module:crypto/deviceinfo} device device info
|
||||
* @property {string?} sessionId base64 olm session id; null if no session
|
||||
* could be established
|
||||
*/
|
||||
@@ -622,7 +754,7 @@ Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
|
||||
|
||||
var userRes = res.one_time_keys[userId] || {};
|
||||
var deviceRes = userRes[deviceId];
|
||||
var oneTimeKey;
|
||||
var oneTimeKey = null;
|
||||
for (var keyId in deviceRes) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
oneTimeKey = deviceRes[keyId];
|
||||
@@ -700,10 +832,18 @@ Crypto.prototype.encryptEventIfNeeded = function(event, room) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We can claim and prove ownership of all our device keys in the local
|
||||
// echo of the event since we know that all the local echos come from
|
||||
// this device.
|
||||
var myKeys = {
|
||||
curve25519: this._olmDevice.deviceCurve25519Key,
|
||||
ed25519: this._olmDevice.deviceEd25519Key,
|
||||
};
|
||||
|
||||
return alg.encryptMessage(
|
||||
room, event.getType(), event.getContent()
|
||||
).then(function(encryptedContent) {
|
||||
event.makeEncrypted("m.room.encrypted", encryptedContent);
|
||||
event.makeEncrypted("m.room.encrypted", encryptedContent, myKeys);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -725,16 +865,160 @@ Crypto.prototype.decryptEvent = function(event) {
|
||||
var alg = new AlgClass({
|
||||
olmDevice: this._olmDevice,
|
||||
});
|
||||
return alg.decryptEvent(event);
|
||||
var r = alg.decryptEvent(event);
|
||||
var payload = r.result;
|
||||
if (r.sessionExists) {
|
||||
payload.keysClaimed = r.keysClaimed;
|
||||
payload.keysProved = r.keysProved;
|
||||
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() {});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* handle an m.room.encryption event
|
||||
*
|
||||
* @private
|
||||
* @param {module:models/event.MatrixEvent} event encryption event
|
||||
*/
|
||||
Crypto.prototype._onCryptoEvent = function(event) {
|
||||
var roomId = event.getRoomId();
|
||||
var content = event.getContent();
|
||||
|
||||
try {
|
||||
this.setRoomEncryption(roomId, content);
|
||||
} catch (e) {
|
||||
console.error("Error configuring encryption in room " + roomId +
|
||||
":", e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* handle the completion of the initial sync.
|
||||
*
|
||||
* Announces the new device.
|
||||
*
|
||||
* @private
|
||||
* @param {module:models/room[]} rooms list of rooms the client knows about
|
||||
*/
|
||||
Crypto.prototype._onInitialSyncCompleted = function(rooms) {
|
||||
if (this._sessionStore.getDeviceAnnounced()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we need to tell all the devices in all the rooms we are members of that
|
||||
// we have arrived.
|
||||
// build a list of rooms for each user.
|
||||
var roomsByUser = {};
|
||||
for (var i = 0; i < rooms.length; i++) {
|
||||
var room = rooms[i];
|
||||
|
||||
// check for rooms with encryption enabled
|
||||
var alg = this._roomAlgorithms[room.roomId];
|
||||
if (!alg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ignore any rooms which we have left
|
||||
var me = room.getMember(this._userId);
|
||||
if (!me || (
|
||||
me.membership !== "join" && me.membership !== "invite"
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var members = room.getJoinedMembers();
|
||||
for (var j = 0; j < members.length; j++) {
|
||||
var m = members[j];
|
||||
if (!roomsByUser[m.userId]) {
|
||||
roomsByUser[m.userId] = [];
|
||||
}
|
||||
roomsByUser[m.userId].push(room.roomId);
|
||||
}
|
||||
}
|
||||
|
||||
// build a per-device message for each user
|
||||
var content = {};
|
||||
for (var userId in roomsByUser) {
|
||||
if (!roomsByUser.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
content[userId] = {
|
||||
"*": {
|
||||
device_id: this._deviceId,
|
||||
rooms: roomsByUser[userId],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this._baseApis.sendToDevice(
|
||||
"m.new_device", // OH HAI!
|
||||
content
|
||||
).done(function() {
|
||||
self._sessionStore.setDeviceAnnounced();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a key event
|
||||
*
|
||||
* @param {module:modules/event~MatrixEvent} event key event
|
||||
* @private
|
||||
* @param {module:models/event.MatrixEvent} event key event
|
||||
*/
|
||||
Crypto.prototype.onRoomKeyEvent = function(event) {
|
||||
Crypto.prototype._onRoomKeyEvent = function(event) {
|
||||
var content = event.getContent();
|
||||
var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
|
||||
if (!AlgClass) {
|
||||
@@ -749,7 +1033,75 @@ Crypto.prototype.onRoomKeyEvent = function(event) {
|
||||
};
|
||||
|
||||
/**
|
||||
* @see module:crypto-algorithms/base.DecryptionError
|
||||
* Handle a change in the membership state of a member of a room
|
||||
*
|
||||
* @private
|
||||
* @param {module:models/event.MatrixEvent} event event causing the change
|
||||
* @param {module:models/room-member} member user whose membership changed
|
||||
* @param {string=} oldMembership previous membership
|
||||
*/
|
||||
Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
|
||||
|
||||
// this event handler is registered on the *client* (as opposed to the
|
||||
// room member itself), which means it is only called on changes to the
|
||||
// *live* membership state (ie, it is not called when we back-paginate).
|
||||
//
|
||||
// Further, it is automatically registered and called when new members
|
||||
// arrive in the room.
|
||||
|
||||
var roomId = member.roomId;
|
||||
|
||||
var alg = this._roomAlgorithms[roomId];
|
||||
if (!alg) {
|
||||
// not encrypting in this room
|
||||
return;
|
||||
}
|
||||
|
||||
alg.onRoomMembership(event, member, oldMembership);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Called when a new device announces itself
|
||||
*
|
||||
* @private
|
||||
* @param {module:models/event.MatrixEvent} event announcement event
|
||||
*/
|
||||
Crypto.prototype._onNewDeviceEvent = function(event) {
|
||||
var content = event.getContent();
|
||||
var userId = event.getSender();
|
||||
var deviceId = content.device_id;
|
||||
var rooms = content.rooms;
|
||||
|
||||
if (!rooms || !deviceId) {
|
||||
console.warn("new_device event missing keys");
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.downloadKeys(
|
||||
[userId], true
|
||||
).then(function() {
|
||||
for (var i = 0; i < rooms.length; i++) {
|
||||
var roomId = rooms[i];
|
||||
var alg = self._roomAlgorithms[roomId];
|
||||
if (!alg) {
|
||||
// not encrypting in this room
|
||||
continue;
|
||||
}
|
||||
alg.onNewDevice(userId, deviceId);
|
||||
}
|
||||
}).catch(function(e) {
|
||||
console.error(
|
||||
"Error updating device keys for new device " + userId + ":" +
|
||||
deviceId,
|
||||
e
|
||||
);
|
||||
}).done();
|
||||
};
|
||||
|
||||
/**
|
||||
* @see module:crypto/algorithms/base.DecryptionError
|
||||
*/
|
||||
Crypto.DecryptionError = algorithms.DecryptionError;
|
||||
|
||||
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
* Utilities common to olm encryption algorithms
|
||||
*/
|
||||
|
||||
var utils = require("./utils");
|
||||
var utils = require("../utils");
|
||||
|
||||
/**
|
||||
* matrix algorithm tag for olm
|
||||
@@ -37,7 +37,7 @@ module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
|
||||
* Encrypt an event payload for a list of devices
|
||||
*
|
||||
* @param {string} ourDeviceId
|
||||
* @param {module:OlmDevice} olmDevice olm.js wrapper
|
||||
* @param {module:crypto/OlmDevice} olmDevice olm.js wrapper
|
||||
* @param {string[]} participantKeys list of curve25519 keys to encrypt for
|
||||
* @param {object} payloadFields fields to include in the encrypted payload
|
||||
*
|
||||
@@ -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;
|
||||
@@ -18,6 +18,8 @@ limitations under the License.
|
||||
* @module filter
|
||||
*/
|
||||
|
||||
var FilterComponent = require("./filter-component");
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @param {string} keyNesting
|
||||
@@ -49,6 +51,14 @@ function Filter(userId, filterId) {
|
||||
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.
|
||||
* @return {Object} The filter definition
|
||||
@@ -63,6 +73,88 @@ Filter.prototype.getDefinition = function() {
|
||||
*/
|
||||
Filter.prototype.setDefinition = function(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));
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+3
-1
@@ -34,7 +34,9 @@ module.exports.MatrixClient = require("./client").MatrixClient;
|
||||
module.exports.Room = require("./models/room");
|
||||
/** The {@link module:models/event-timeline~EventTimeline} class. */
|
||||
module.exports.EventTimeline = require("./models/event-timeline");
|
||||
/** The {@link module:models/room-member~RoomMember|RoomMember} class. */
|
||||
/** 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. */
|
||||
module.exports.RoomMember = require("./models/room-member");
|
||||
/** The {@link module:models/room-state~RoomState|RoomState} class. */
|
||||
module.exports.RoomState = require("./models/room-state");
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -25,16 +25,17 @@ var MatrixEvent = require("./event").MatrixEvent;
|
||||
* <p>Once a timeline joins up with its neighbour, they are linked together into a
|
||||
* 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
|
||||
*/
|
||||
function EventTimeline(roomId) {
|
||||
this._roomId = roomId;
|
||||
function EventTimeline(eventTimelineSet) {
|
||||
this._eventTimelineSet = eventTimelineSet;
|
||||
this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null;
|
||||
this._events = [];
|
||||
this._baseIndex = 0;
|
||||
this._startState = new RoomState(roomId);
|
||||
this._startState = new RoomState(this._roomId);
|
||||
this._startState.paginationToken = null;
|
||||
this._endState = new RoomState(roomId);
|
||||
this._endState = new RoomState(this._roomId);
|
||||
this._endState.paginationToken = null;
|
||||
|
||||
this._prevTimeline = null;
|
||||
@@ -43,7 +44,7 @@ function EventTimeline(roomId) {
|
||||
// this is used by client.js
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -217,23 +234,29 @@ EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction)
|
||||
EventTimeline.prototype.addEvent = function(event, atStart) {
|
||||
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
|
||||
if (event.isState()) {
|
||||
stateContext.setStateEvents([event]);
|
||||
// it is possible that the act of setting the state event means we
|
||||
// can set more metadata (specifically sender/target props), so try
|
||||
// it again if the prop wasn't previously set. It may also mean that
|
||||
// the sender/target is updated (if the event set was a room member event)
|
||||
// so we want to use the *updated* member (new avatar/name) instead.
|
||||
//
|
||||
// However, we do NOT want to do this on member events if we're going
|
||||
// back in time, else we'll set the .sender value for BEFORE the given
|
||||
// member event, whereas we want to set the .sender value for the ACTUAL
|
||||
// member event itself.
|
||||
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
|
||||
setEventMetadata(event, stateContext, atStart);
|
||||
// modify state
|
||||
if (event.isState()) {
|
||||
stateContext.setStateEvents([event]);
|
||||
// it is possible that the act of setting the state event means we
|
||||
// can set more metadata (specifically sender/target props), so try
|
||||
// it again if the prop wasn't previously set. It may also mean that
|
||||
// the sender/target is updated (if the event set was a room member event)
|
||||
// so we want to use the *updated* member (new avatar/name) instead.
|
||||
//
|
||||
// However, we do NOT want to do this on member events if we're going
|
||||
// back in time, else we'll set the .sender value for BEFORE the given
|
||||
// member event, whereas we want to set the .sender value for the ACTUAL
|
||||
// member event itself.
|
||||
if (!event.sender || (event.getType() === "m.room.member" && !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
|
||||
event.sender = stateContext.getSentinelMember(
|
||||
event.getSender()
|
||||
@@ -270,7 +300,7 @@ function setEventMetadata(event, stateContext, toStartOfTimeline) {
|
||||
event.forwardLooking = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an event from the timeline
|
||||
|
||||
+72
-8
@@ -76,6 +76,7 @@ module.exports.MatrixEvent = function MatrixEvent(event, clearEvent) {
|
||||
this.forwardLooking = true;
|
||||
|
||||
this._clearEvent = clearEvent || {};
|
||||
this._pushActions = null;
|
||||
};
|
||||
|
||||
module.exports.MatrixEvent.prototype = {
|
||||
@@ -213,12 +214,15 @@ module.exports.MatrixEvent.prototype = {
|
||||
* <tt>"m.room.encrypted"</tt>
|
||||
*
|
||||
* @param {object} crypto_content raw 'content' for the encrypted event.
|
||||
* @param {object} keys The local keys claimed and proved by this event.
|
||||
*/
|
||||
makeEncrypted: function(crypto_type, crypto_content) {
|
||||
makeEncrypted: function(crypto_type, crypto_content, keys) {
|
||||
// keep the plain-text data for 'view source'
|
||||
this._clearEvent = {
|
||||
type: this.event.type,
|
||||
content: this.event.content,
|
||||
keysProved: keys,
|
||||
keysClaimed: keys,
|
||||
};
|
||||
this.event.type = crypto_type;
|
||||
this.event.content = crypto_content;
|
||||
@@ -232,12 +236,42 @@ module.exports.MatrixEvent.prototype = {
|
||||
return Boolean(this._clearEvent.type);
|
||||
},
|
||||
|
||||
/**
|
||||
* The curve25519 key that sent this event
|
||||
* @return {string}
|
||||
*/
|
||||
getSenderKey: function() {
|
||||
if (!this.isEncrypted()) {
|
||||
return null;
|
||||
}
|
||||
var c = this.getWireContent();
|
||||
return c.sender_key;
|
||||
return this.getKeysProved().curve25519 || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* The keys that must have been owned by the sender of this encrypted event.
|
||||
* These don't necessarily have to come from this event itself, but may be
|
||||
* implied by the cryptographic session.
|
||||
* For example megolm messages don't prove keys directly, but instead
|
||||
* inherit a proof from the olm message that established the session.
|
||||
* @return {object}
|
||||
*/
|
||||
getKeysProved: function() {
|
||||
// The keysProved property usually isn't actually part of the decrypted
|
||||
// plaintext. Instead it is added after decryption by the crypto
|
||||
// algorithm in lib/crypto/algorithms.
|
||||
return this._clearEvent.keysProved || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* The additional keys the sender of this encrypted event claims to possess
|
||||
* These don't necessarily have to come from this event itself, but may be
|
||||
* implied by the cryptographic session.
|
||||
* For example megolm messages don't claim keys directly, but instead
|
||||
* inherit a claim from the olm message that established the session.
|
||||
* @return {object}
|
||||
*/
|
||||
getKeysClaimed: function() {
|
||||
// The keysClaimed property usually isn't actually part of the
|
||||
// decrypted plaintext. Instead it is added after decryption by the
|
||||
// crypto algorithm in lib/crypto/algorithms.
|
||||
return this._clearEvent.keysClaimed || {};
|
||||
},
|
||||
|
||||
getUnsigned: function() {
|
||||
@@ -248,13 +282,25 @@ module.exports.MatrixEvent.prototype = {
|
||||
* Update the content of an event in the same way it would be by the server
|
||||
* if it were redacted before it was sent to us
|
||||
*
|
||||
* @param {Object} the raw event causing the redaction
|
||||
* @param {module:models/event.MatrixEvent} redaction_event
|
||||
* event causing the redaction
|
||||
*/
|
||||
makeRedacted: function(redaction_event) {
|
||||
// quick sanity-check
|
||||
if (!redaction_event.event) {
|
||||
throw new Error("invalid redaction_event in makeRedacted");
|
||||
}
|
||||
|
||||
// we attempt to replicate what we would see from the server if
|
||||
// the event had been redacted before we saw it.
|
||||
//
|
||||
// The server removes (most of) the content of the event, and adds a
|
||||
// "redacted_because" key to the unsigned section containing the
|
||||
// redacted event.
|
||||
if (!this.event.unsigned) {
|
||||
this.event.unsigned = {};
|
||||
}
|
||||
this.event.unsigned.redacted_because = redaction_event;
|
||||
this.event.unsigned.redacted_because = redaction_event.event;
|
||||
|
||||
var key;
|
||||
for (key in this.event) {
|
||||
@@ -282,6 +328,24 @@ module.exports.MatrixEvent.prototype = {
|
||||
isRedacted: function() {
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
+10
-18
@@ -24,7 +24,10 @@ var utils = require("../utils");
|
||||
|
||||
/**
|
||||
* Construct a new room member.
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:models/room-member
|
||||
*
|
||||
* @param {string} roomId The room ID of the member.
|
||||
* @param {string} userId The user ID of the member.
|
||||
* @prop {string} roomId The room ID for this member.
|
||||
@@ -77,11 +80,11 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) {
|
||||
this.name = calculateDisplayName(this, event, roomState);
|
||||
if (oldMembership !== this.membership) {
|
||||
this._updateModifiedTime();
|
||||
this.emit("RoomMember.membership", event, this);
|
||||
this.emit("RoomMember.membership", event, this, oldMembership);
|
||||
}
|
||||
if (oldName !== this.name) {
|
||||
this._updateModifiedTime();
|
||||
this.emit("RoomMember.name", event, this);
|
||||
this.emit("RoomMember.name", event, this, oldName);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -209,21 +212,6 @@ function calculateDisplayName(member, event, roomState) {
|
||||
var displayName = event.getDirectionalContent().displayname;
|
||||
var selfUserId = member.userId;
|
||||
|
||||
/*
|
||||
// FIXME: this would be great but still needs to use the
|
||||
// full userId to disambiguate if needed...
|
||||
|
||||
if (!displayName) {
|
||||
var matches = selfUserId.match(/^@(.*?):/);
|
||||
if (matches) {
|
||||
return matches[1];
|
||||
}
|
||||
else {
|
||||
return selfUserId;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
if (!displayName) {
|
||||
return selfUserId;
|
||||
}
|
||||
@@ -252,6 +240,8 @@ module.exports = RoomMember;
|
||||
* @event module:client~MatrixClient#"RoomMember.name"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomMember} member The member whose RoomMember.name changed.
|
||||
* @param {string?} oldName The previous name. Null if the member didn't have a
|
||||
* name previously.
|
||||
* @example
|
||||
* matrixClient.on("RoomMember.name", function(event, member){
|
||||
* var newName = member.name;
|
||||
@@ -263,8 +253,10 @@ module.exports = RoomMember;
|
||||
* @event module:client~MatrixClient#"RoomMember.membership"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomMember} member The member whose RoomMember.membership changed.
|
||||
* @param {string?} oldMembership The previous membership state. Null if it's a
|
||||
* new member.
|
||||
* @example
|
||||
* matrixClient.on("RoomMember.membership", function(event, member){
|
||||
* matrixClient.on("RoomMember.membership", function(event, member, oldMembership){
|
||||
* var newState = member.membership;
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -25,7 +25,8 @@ var RoomMember = require("./room-member");
|
||||
/**
|
||||
* Construct room state.
|
||||
* @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
|
||||
* on the user's ID.
|
||||
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
|
||||
|
||||
+214
-480
@@ -25,17 +25,7 @@ var MatrixEvent = require("./event").MatrixEvent;
|
||||
var utils = require("../utils");
|
||||
var ContentRepo = require("../content-repo");
|
||||
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() {};
|
||||
}
|
||||
var EventTimelineSet = require("./event-timeline-set");
|
||||
|
||||
|
||||
function synthesizeReceipt(userId, event, receiptType) {
|
||||
@@ -159,13 +149,18 @@ function Room(roomId, opts) {
|
||||
|
||||
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();
|
||||
|
||||
// just a list - *not* ordered.
|
||||
this._timelines = [this._liveTimeline];
|
||||
this._eventIdToTimeline = {};
|
||||
this._timelineSupport = Boolean(opts.timelineSupport);
|
||||
// any filtered timeline sets we're maintaining for this room
|
||||
this._filteredTimelineSets = {
|
||||
// filter_id: timelineSet
|
||||
};
|
||||
|
||||
if (this._opts.pendingEventOrdering == "detached") {
|
||||
this._pendingEventList = [];
|
||||
@@ -191,57 +186,29 @@ Room.prototype.getPendingEvents = function() {
|
||||
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
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param {string=} backPaginationToken token for back-paginating the new timeline
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.timelineReset"
|
||||
*/
|
||||
Room.prototype.resetLiveTimeline = function(backPaginationToken) {
|
||||
var newTimeline;
|
||||
|
||||
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();
|
||||
for (var i = 0; i < this._timelineSets.length; i++) {
|
||||
this._timelineSets[i].resetLiveTimeline(backPaginationToken);
|
||||
}
|
||||
|
||||
// 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.emit("Room.timelineReset", this);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -254,38 +221,58 @@ Room.prototype._fixUpLegacyTimelineFields = function() {
|
||||
// and this.oldState and this.currentState as references to the
|
||||
// state at the start and end of that timeline. These are more
|
||||
// for backwards-compatibility than anything else.
|
||||
this.timeline = this._liveTimeline.getEvents();
|
||||
this.oldState = this._liveTimeline.getState(EventTimeline.BACKWARDS);
|
||||
this.currentState = this._liveTimeline.getState(EventTimeline.FORWARDS);
|
||||
this.timeline = this.getLiveTimeline().getEvents();
|
||||
this.oldState = this.getLiveTimeline()
|
||||
.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
|
||||
* @return {?module:models/event-timeline~EventTimeline} timeline containing
|
||||
* the given event, or null if unknown
|
||||
*/
|
||||
Room.prototype.getTimelineForEvent = function(eventId) {
|
||||
var res = this._eventIdToTimeline[eventId];
|
||||
return (res === undefined) ? null : res;
|
||||
return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an event which is stored in our timelines
|
||||
* Add a new timeline to this room's unfiltered timeline set
|
||||
*
|
||||
* @param {string} eventId event ID to look for
|
||||
* @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown
|
||||
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
|
||||
*/
|
||||
Room.prototype.findEventById = function(eventId) {
|
||||
var tl = this.getTimelineForEvent(eventId);
|
||||
if (!tl) {
|
||||
return undefined;
|
||||
}
|
||||
return utils.findElement(tl.getEvents(),
|
||||
function(ev) { return ev.getId() == eventId; });
|
||||
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
|
||||
* @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown
|
||||
*/
|
||||
Room.prototype.findEventById = function(eventId) {
|
||||
return this.getUnfilteredTimelineSet().findEventById(eventId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get one of the notification counts for this room
|
||||
@@ -379,6 +366,33 @@ Room.prototype.getCanonicalAlias = function() {
|
||||
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.
|
||||
* @param {string} userId The user ID of the member.
|
||||
@@ -438,224 +452,72 @@ Room.prototype.getCanonicalAlias = function() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new timeline to this room
|
||||
*
|
||||
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
|
||||
* Add a timelineSet for this room with the given filter
|
||||
* @param {Filter} filter The filter to be applied to this timelineSet
|
||||
* @return {EventTimelineSet} The timelineSet
|
||||
*/
|
||||
Room.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.");
|
||||
Room.prototype.getOrCreateFilteredTimelineSet = function(filter) {
|
||||
if (this._filteredTimelineSets[filter.filterId]) {
|
||||
return this._filteredTimelineSets[filter.filterId];
|
||||
}
|
||||
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);
|
||||
this._timelines.push(timeline);
|
||||
return timeline;
|
||||
timelineSet.getLiveTimeline().setPaginationToken(
|
||||
timeline.getPaginationToken(EventTimeline.BACKWARDS),
|
||||
EventTimeline.BACKWARDS
|
||||
);
|
||||
|
||||
// alternatively, we could try to do something like this to try and re-paginate
|
||||
// in the filtered events from nothing, but Mark says it's an abuse of the API
|
||||
// to do so:
|
||||
//
|
||||
// timelineSet.resetLiveTimeline(
|
||||
// unfilteredLiveTimeline.getPaginationToken(EventTimeline.FORWARDS)
|
||||
// );
|
||||
|
||||
return timelineSet;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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"
|
||||
* Forget the timelineSet for this room with the given filter
|
||||
*
|
||||
* @param {Filter} filter the filter whose timelineSet is to be forgotten
|
||||
*/
|
||||
Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
timeline, paginationToken) {
|
||||
if (!timeline) {
|
||||
throw new Error(
|
||||
"'timeline' not specified for Room.addEventsToTimeline"
|
||||
);
|
||||
}
|
||||
|
||||
if (!toStartOfTimeline && timeline == this._liveTimeline) {
|
||||
throw new Error(
|
||||
"Room.addEventsToTimeline cannot be used for adding events to " +
|
||||
"the live timeline - use Room.addLiveEvents instead"
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
Room.prototype.removeFilteredTimelineSet = function(filter) {
|
||||
var timelineSet = this._filteredTimelineSets[filter.filterId];
|
||||
delete this._filteredTimelineSets[filter.filterId];
|
||||
var i = this._timelineSets.indexOf(timelineSet);
|
||||
if (i > -1) {
|
||||
this._timelineSets.splice(i, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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"
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Room.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, Boolean(toStartOfTimeline), false, data);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Add an event to the end of this room's live timeline. Will fire
|
||||
* "Room.timeline"..
|
||||
* Add an event to the end of this room's live timelines. Will fire
|
||||
* "Room.timeline".
|
||||
*
|
||||
* @param {MatrixEvent} event Event to be added
|
||||
* @param {string?} duplicateStrategy 'ignore' or 'replace'
|
||||
@@ -663,11 +525,12 @@ Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline
|
||||
* @private
|
||||
*/
|
||||
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
||||
var i;
|
||||
if (event.getType() === "m.room.redaction") {
|
||||
var redactId = event.event.redacts;
|
||||
|
||||
// if we know about this event, redact its contents now.
|
||||
var redactedEvent = this.findEventById(redactId);
|
||||
var redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
|
||||
if (redactedEvent) {
|
||||
redactedEvent.makeRedacted(event);
|
||||
this.emit("Room.redaction", event, this);
|
||||
@@ -679,6 +542,8 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
||||
// they are based on are changed.
|
||||
}
|
||||
|
||||
// FIXME: apply redactions to notification list
|
||||
|
||||
// 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
|
||||
// this may be needed to trigger an update.
|
||||
@@ -693,39 +558,11 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
|
||||
}
|
||||
}
|
||||
|
||||
var timeline = this._eventIdToTimeline[event.getId()];
|
||||
if (timeline) {
|
||||
if (duplicateStrategy === "replace") {
|
||||
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;
|
||||
// add to our timeline sets
|
||||
for (i = 0; i < this._timelineSets.length; i++) {
|
||||
this._timelineSets[i].addLiveEvent(event, duplicateStrategy);
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
@@ -773,9 +610,11 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
}
|
||||
|
||||
// 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,
|
||||
this._liveTimeline.getState(EventTimeline.FORWARDS),
|
||||
this.getLiveTimeline().getState(EventTimeline.FORWARDS),
|
||||
false
|
||||
);
|
||||
|
||||
@@ -784,7 +623,19 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
if (this._opts.pendingEventOrdering == "detached") {
|
||||
this._pendingEventList.push(event);
|
||||
} 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);
|
||||
@@ -796,9 +647,9 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
* <p>We move the event to the live timeline if it isn't there already, and
|
||||
* update it.
|
||||
*
|
||||
* @param {module:models/event~MatrixEvent} remoteEvent The event received from
|
||||
* @param {module:models/event.MatrixEvent} remoteEvent The event received from
|
||||
* /sync
|
||||
* @param {module:models/event~MatrixEvent} localEvent The local echo, which
|
||||
* @param {module:models/event.MatrixEvent} localEvent The local echo, which
|
||||
* should be either in the _pendingEventList or the timeline.
|
||||
*
|
||||
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
|
||||
@@ -828,13 +679,11 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
|
||||
// successfully sent.
|
||||
localEvent.status = null;
|
||||
|
||||
// if it's already in the timeline, update the timeline map. If it's not, add it.
|
||||
var existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||
if (existingTimeline) {
|
||||
delete this._eventIdToTimeline[oldEventId];
|
||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||
} else {
|
||||
this._addEventToTimeline(localEvent, this._liveTimeline, false);
|
||||
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.
|
||||
timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
||||
}
|
||||
|
||||
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.
|
||||
if (newStatus == EventStatus.SENT) {
|
||||
var timeline = this._eventIdToTimeline[newEventId];
|
||||
var timeline = this.getUnfilteredTimelineSet().eventIdToTimeline(newEventId);
|
||||
if (timeline) {
|
||||
// we've already received the event via the event stream.
|
||||
// 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
|
||||
// opts.pendingEventOrdering==chronological), we need to update the
|
||||
// timeline map.
|
||||
var existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||
if (existingTimeline) {
|
||||
delete this._eventIdToTimeline[oldEventId];
|
||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||
for (var i = 0; i < this._timelineSets.length; i++) {
|
||||
this._timelineSets[i].replaceEventId(oldEventId, newEventId);
|
||||
}
|
||||
}
|
||||
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'.
|
||||
*/
|
||||
Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
|
||||
var i;
|
||||
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
||||
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
||||
}
|
||||
|
||||
// sanity check that the live timeline is still live
|
||||
if (this._liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
|
||||
throw new Error(
|
||||
"live timeline is no longer live - it has a pagination token (" +
|
||||
this._liveTimeline.getPaginationToken(EventTimeline.FORWARDS) + ")"
|
||||
);
|
||||
}
|
||||
if (this._liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
|
||||
throw new Error(
|
||||
"live timeline is no longer live - it has a neighbouring timeline"
|
||||
);
|
||||
for (i = 0; i < this._timelineSets.length; i++) {
|
||||
var liveTimeline = this._timelineSets[i].getLiveTimeline();
|
||||
if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
|
||||
throw new Error(
|
||||
"live timeline " + i + " is no longer live - it has a pagination token " +
|
||||
"(" + liveTimeline.getPaginationToken(EventTimeline.FORWARDS) + ")"
|
||||
);
|
||||
}
|
||||
if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
|
||||
throw new Error(
|
||||
"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") {
|
||||
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
|
||||
*
|
||||
* @return {?MatrixEvent} the removed event, or null if the event was not found
|
||||
* in this room.
|
||||
* @return {bool} true if the event was removed from any of the room's timeline sets
|
||||
*/
|
||||
Room.prototype.removeEvent = function(eventId) {
|
||||
var timeline = this._eventIdToTimeline[eventId];
|
||||
if (!timeline) {
|
||||
return null;
|
||||
var removedAny = false;
|
||||
for (var i = 0; i < this._timelineSets.length; i++) {
|
||||
var removed = this._timelineSets[i].removeEvent(eventId);
|
||||
if (removed) {
|
||||
removedAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
var removed = timeline.removeEvent(eventId);
|
||||
if (removed) {
|
||||
delete this._eventIdToTimeline[eventId];
|
||||
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
|
||||
@@ -1221,7 +994,7 @@ Room.prototype.addReceipt = function(event, fake) {
|
||||
// as there's nothing that would read it.
|
||||
}
|
||||
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
|
||||
// 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
|
||||
// server-side, but because we synthesize RRs locally we
|
||||
// have to do it here too.)
|
||||
var ordering = self.compareEventOrdering(
|
||||
var ordering = self.getUnfilteredTimelineSet().compareEventOrdering(
|
||||
existingReceipt.eventId, eventId);
|
||||
if (ordering !== null && ordering >= 0) {
|
||||
return;
|
||||
@@ -1275,7 +1048,7 @@ Room.prototype._addReceiptsToStructure = function(event, receipts) {
|
||||
* @param {Object} receipts A map of receipts
|
||||
* @return {Object} Map of receipts by event ID
|
||||
*/
|
||||
Room.prototype._buildReciptCache = function(receipts) {
|
||||
Room.prototype._buildReceiptCache = function(receipts) {
|
||||
var receiptCacheByEventId = {};
|
||||
utils.keys(receipts).forEach(function(receiptType) {
|
||||
utils.keys(receipts[receiptType]).forEach(function(userId) {
|
||||
@@ -1350,27 +1123,6 @@ Room.prototype.getAccountData = function(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
|
||||
* room state.
|
||||
@@ -1486,48 +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.
|
||||
*/
|
||||
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, 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.
|
||||
*
|
||||
|
||||
@@ -47,6 +47,7 @@ function User(userId) {
|
||||
this.presence = "offline";
|
||||
this.presenceStatusMsg = null;
|
||||
this.displayName = userId;
|
||||
this.rawDisplayName = userId;
|
||||
this.avatarUrl = null;
|
||||
this.lastActiveAgo = 0;
|
||||
this.lastPresenceTs = 0;
|
||||
@@ -131,6 +132,17 @@ User.prototype.setDisplayName = function(name) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Manually set this user's non-disambiguated display name. No event is emitted
|
||||
* in response to this as there is no underlying MatrixEvent to emit with.
|
||||
* @param {string} name The new display name.
|
||||
*/
|
||||
User.prototype.setRawDisplayName = function(name) {
|
||||
this.rawDisplayName = name;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Manually set this user's avatar URL. No event is emitted in response to this
|
||||
* as there is no underlying MatrixEvent to emit with.
|
||||
|
||||
@@ -101,6 +101,11 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
var user = this.users[member.userId] || new User(member.userId);
|
||||
if (member.name) {
|
||||
user.setDisplayName(member.name);
|
||||
if (member.events.member) {
|
||||
user.setRawDisplayName(
|
||||
member.events.member.getDirectionalContent().displayname
|
||||
);
|
||||
}
|
||||
}
|
||||
if (member.events.member && member.events.member.getContent().avatar_url) {
|
||||
user.setAvatarUrl(member.events.member.getContent().avatar_url);
|
||||
|
||||
@@ -62,6 +62,22 @@ WebStorageSessionStore.prototype = {
|
||||
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a flag indicating that we have announced the new device.
|
||||
*/
|
||||
setDeviceAnnounced: function() {
|
||||
this.store.setItem(KEY_END_TO_END_ANNOUNCED, "true");
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the "device announced" flag is set
|
||||
*
|
||||
* @return {boolean} true if the "device announced" flag has been set.
|
||||
*/
|
||||
getDeviceAnnounced: function() {
|
||||
return this.store.getItem(KEY_END_TO_END_ANNOUNCED) == "true";
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the known devices for a user.
|
||||
* @param {string} userId The user's ID.
|
||||
@@ -134,6 +150,7 @@ WebStorageSessionStore.prototype = {
|
||||
};
|
||||
|
||||
var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
var KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
|
||||
|
||||
function keyEndToEndDevicesForUser(userId) {
|
||||
return E2E_PREFIX + "devices/" + userId;
|
||||
|
||||
+103
-79
@@ -72,6 +72,12 @@ function SyncApi(client, opts) {
|
||||
this._running = false;
|
||||
this._keepAliveTimer = 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
|
||||
};
|
||||
|
||||
return this._getOrCreateFilter(
|
||||
return client.getOrCreateFilter(
|
||||
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter
|
||||
).then(function(filterId) {
|
||||
qps.filter = filterId;
|
||||
@@ -389,9 +395,15 @@ SyncApi.prototype.sync = function() {
|
||||
var filter = new Filter(client.credentials.userId);
|
||||
filter.setTimelineLimit(self.opts.initialSyncLimit);
|
||||
|
||||
self._getOrCreateFilter(
|
||||
client.getOrCreateFilter(
|
||||
getFilterName(client.credentials.userId), filter
|
||||
).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 });
|
||||
}, function(err) {
|
||||
self._startKeepAlives().done(function() {
|
||||
@@ -496,36 +508,6 @@ SyncApi.prototype._sync = function(syncOptions) {
|
||||
|
||||
this._currentSyncRequest.done(function(data) {
|
||||
self._syncConnectionLost = false;
|
||||
// data looks like:
|
||||
// {
|
||||
// next_batch: $token,
|
||||
// presence: { events: [] },
|
||||
// rooms: {
|
||||
// invite: {
|
||||
// $roomid: {
|
||||
// invite_state: { events: [] }
|
||||
// }
|
||||
// },
|
||||
// join: {
|
||||
// $roomid: {
|
||||
// state: { events: [] },
|
||||
// timeline: { events: [], prev_batch: $token, limited: true },
|
||||
// ephemeral: { events: [] },
|
||||
// account_data: { events: [] },
|
||||
// unread_notifications: {
|
||||
// highlight_count: 0,
|
||||
// notification_count: 0,
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// leave: {
|
||||
// $roomid: {
|
||||
// state: { events: [] },
|
||||
// timeline: { events: [], prev_batch: $token }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// set the sync token NOW *before* processing the events. We do this so
|
||||
// if something barfs on an event we can skip it rather than constantly
|
||||
@@ -585,6 +567,39 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
var client = this.client;
|
||||
var self = this;
|
||||
|
||||
// data looks like:
|
||||
// {
|
||||
// next_batch: $token,
|
||||
// presence: { events: [] },
|
||||
// account_data: { events: [] },
|
||||
// to_device: { events: [] },
|
||||
// rooms: {
|
||||
// invite: {
|
||||
// $roomid: {
|
||||
// invite_state: { events: [] }
|
||||
// }
|
||||
// },
|
||||
// join: {
|
||||
// $roomid: {
|
||||
// state: { events: [] },
|
||||
// timeline: { events: [], prev_batch: $token, limited: true },
|
||||
// ephemeral: { events: [] },
|
||||
// account_data: { events: [] },
|
||||
// unread_notifications: {
|
||||
// highlight_count: 0,
|
||||
// notification_count: 0,
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// leave: {
|
||||
// $roomid: {
|
||||
// state: { events: [] },
|
||||
// timeline: { events: [], prev_batch: $token }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
|
||||
// TODO-arch:
|
||||
// - Each event we pass through needs to be emitted via 'event', can we
|
||||
// do this in one place?
|
||||
@@ -622,6 +637,28 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
);
|
||||
}
|
||||
|
||||
// handle to-device events
|
||||
if (data.to_device && utils.isArray(data.to_device.events)) {
|
||||
data.to_device.events
|
||||
.map(client.getEventMapper())
|
||||
.forEach(
|
||||
function(toDeviceEvent) {
|
||||
var content = toDeviceEvent.getContent();
|
||||
if (
|
||||
toDeviceEvent.getType() == "m.room.message" &&
|
||||
content.msgtype == "m.bad.encrypted"
|
||||
) {
|
||||
console.warn(
|
||||
"Unable to decrypt to-device event: " + content.body
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit("toDeviceEvent", toDeviceEvent);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// the returned json structure is a bit crap, so make it into a
|
||||
// nicer form (array) after applying sanity to make sure we don't fail
|
||||
// on missing keys (on the off chance)
|
||||
@@ -641,6 +678,8 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
}
|
||||
}
|
||||
|
||||
this._notifEvents = [];
|
||||
|
||||
// Handle invites
|
||||
inviteRooms.forEach(function(inviteObj) {
|
||||
var room = inviteObj.room;
|
||||
@@ -726,6 +765,12 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
room.currentState.paginationToken = syncToken;
|
||||
self._deregisterStateListeners(room);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -774,6 +819,20 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
timelineEvents.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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -837,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
|
||||
* @return {Object[]}
|
||||
@@ -1009,6 +1021,18 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
|
||||
// may make notifications appear which should have the right name.
|
||||
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
|
||||
// if the timeline has any state events in it.
|
||||
room.addLiveEvents(timelineEventList);
|
||||
|
||||
@@ -56,7 +56,7 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
|
||||
* @param {MatrixClient} client MatrixClient to be used for context/pagination
|
||||
* requests.
|
||||
*
|
||||
* @param {Room} room The room to track
|
||||
* @param {EventTimelineSet} timelineSet The timelineSet to track
|
||||
*
|
||||
* @param {Object} [opts] Configuration options for this window
|
||||
*
|
||||
@@ -66,10 +66,10 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function TimelineWindow(client, room, opts) {
|
||||
function TimelineWindow(client, timelineSet, opts) {
|
||||
opts = opts || {};
|
||||
this._client = client;
|
||||
this._room = room;
|
||||
this._timelineSet = timelineSet;
|
||||
|
||||
// these will be TimelineIndex objects; they delineate the 'start' and
|
||||
// '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
|
||||
// skip straight to the find-event loop.
|
||||
if (initialEventId) {
|
||||
return this._client.getEventTimeline(this._room, initialEventId)
|
||||
return this._client.getEventTimeline(this._timelineSet, initialEventId)
|
||||
.then(function(tl) {
|
||||
// make sure that our window includes the event
|
||||
for (var i = 0; i < tl.getEvents().length; i++) {
|
||||
@@ -126,7 +126,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
||||
});
|
||||
} else {
|
||||
// start with the most recent events
|
||||
var tl = this._room.getLiveTimeline();
|
||||
var tl = this._timelineSet.getLiveTimeline();
|
||||
initFields(tl, tl.getEvents().length);
|
||||
return q();
|
||||
}
|
||||
@@ -254,6 +254,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
||||
|
||||
debuglog("TimelineWindow: starting request");
|
||||
var self = this;
|
||||
|
||||
var prom = this._client.paginateEventTimeline(tl.timeline, {
|
||||
backwards: direction == EventTimeline.BACKWARDS,
|
||||
limit: size
|
||||
|
||||
+128
-18
@@ -60,6 +60,12 @@ function MatrixCall(opts) {
|
||||
this.candidateSendQueue = [];
|
||||
this.candidateSendTries = 0;
|
||||
|
||||
// Lookup from opaque queue ID to a promise for media element operations that
|
||||
// need to be serialised into a given queue. Store this per-MatrixCall on the
|
||||
// assumption that multiple matrix calls will never compete for control of the
|
||||
// same DOM elements.
|
||||
this.mediaPromises = Object.create(null);
|
||||
|
||||
this.screenSharingStream = null;
|
||||
}
|
||||
/** The length of time a call can be ringing for. */
|
||||
@@ -144,6 +150,86 @@ MatrixCall.prototype.placeScreenSharingCall =
|
||||
_tryPlayRemoteStream(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Play the given HTMLMediaElement, serialising the operation into a chain
|
||||
* of promises to avoid racing access to the element
|
||||
* @param {Element} HTMLMediaElement element to play
|
||||
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
|
||||
*/
|
||||
MatrixCall.prototype.playElement = function(element, queueId) {
|
||||
console.log("queuing play on " + queueId + " and element " + element);
|
||||
// XXX: FIXME: Does this leak elements, given the old promises
|
||||
// may hang around and retain a reference to them?
|
||||
if (this.mediaPromises[queueId]) {
|
||||
// XXX: these promises can fail (e.g. by <video/> being unmounted whilst
|
||||
// pending receiving media to play - e.g. whilst switching between
|
||||
// rooms before answering an inbound call), and throw unhandled exceptions.
|
||||
// However, we should soldier on as best we can even if they fail, given
|
||||
// these failures may be non-fatal (as in the case of unmounts)
|
||||
this.mediaPromises[queueId] =
|
||||
this.mediaPromises[queueId].then(function() {
|
||||
console.log("previous promise completed for " + queueId);
|
||||
return element.play();
|
||||
}, function() {
|
||||
console.log("previous promise failed for " + queueId);
|
||||
return element.play();
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.mediaPromises[queueId] = element.play();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Pause the given HTMLMediaElement, serialising the operation into a chain
|
||||
* of promises to avoid racing access to the element
|
||||
* @param {Element} HTMLMediaElement element to pause
|
||||
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
|
||||
*/
|
||||
MatrixCall.prototype.pauseElement = function(element, queueId) {
|
||||
console.log("queuing pause on " + queueId + " and element " + element);
|
||||
if (this.mediaPromises[queueId]) {
|
||||
this.mediaPromises[queueId] =
|
||||
this.mediaPromises[queueId].then(function() {
|
||||
console.log("previous promise completed for " + queueId);
|
||||
return element.pause();
|
||||
}, function() {
|
||||
console.log("previous promise failed for " + queueId);
|
||||
return element.pause();
|
||||
});
|
||||
}
|
||||
else {
|
||||
// pause doesn't actually return a promise, but do this for symmetry
|
||||
// and just in case it does in future.
|
||||
this.mediaPromises[queueId] = element.pause();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign the given HTMLMediaElement by setting the .src attribute on it,
|
||||
* serialising the operation into a chain of promises to avoid racing access
|
||||
* to the element
|
||||
* @param {Element} HTMLMediaElement element to pause
|
||||
* @param {string} src the src attribute value to assign to the element
|
||||
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
|
||||
*/
|
||||
MatrixCall.prototype.assignElement = function(element, src, queueId) {
|
||||
console.log("queuing assign on " + queueId + " element " + element + " for " + src);
|
||||
if (this.mediaPromises[queueId]) {
|
||||
this.mediaPromises[queueId] =
|
||||
this.mediaPromises[queueId].then(function() {
|
||||
console.log("previous promise completed for " + queueId);
|
||||
element.src = src;
|
||||
}, function() {
|
||||
console.log("previous promise failed for " + queueId);
|
||||
element.src = src;
|
||||
});
|
||||
}
|
||||
else {
|
||||
element.src = src;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the local <code><video></code> DOM element.
|
||||
* @return {Element} The dom element
|
||||
@@ -180,13 +266,15 @@ MatrixCall.prototype.setLocalVideoElement = function(element) {
|
||||
|
||||
if (element && this.localAVStream && this.type === 'video') {
|
||||
element.autoplay = true;
|
||||
element.src = this.URL.createObjectURL(this.localAVStream);
|
||||
this.assignElement(element,
|
||||
this.URL.createObjectURL(this.localAVStream),
|
||||
"localVideo");
|
||||
element.muted = true;
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
var vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
vel.play();
|
||||
self.playElement(vel, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
@@ -205,9 +293,11 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
|
||||
/**
|
||||
* Set the remote <code><audio></code> DOM element. If this call is active,
|
||||
* the first received audio-only stream will be rendered to it immediately.
|
||||
* The audio will *not* be rendered from the remoteVideoElement.
|
||||
* @param {Element} element The <code><video></code> DOM element.
|
||||
*/
|
||||
MatrixCall.prototype.setRemoteAudioElement = function(element) {
|
||||
this.remoteVideoElement.muted = true;
|
||||
this.remoteAudioElement = element;
|
||||
_tryPlayRemoteAudioStream(this);
|
||||
};
|
||||
@@ -414,16 +504,20 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
|
||||
videoEl.autoplay = true;
|
||||
if (this.screenSharingStream) {
|
||||
debuglog("Setting screen sharing stream to the local video element");
|
||||
videoEl.src = this.URL.createObjectURL(this.screenSharingStream);
|
||||
this.assignElement(videoEl,
|
||||
this.URL.createObjectURL(this.screenSharingStream),
|
||||
"localVideo");
|
||||
}
|
||||
else {
|
||||
videoEl.src = this.URL.createObjectURL(stream);
|
||||
this.assignElement(videoEl,
|
||||
this.URL.createObjectURL(stream),
|
||||
"localVideo");
|
||||
}
|
||||
videoEl.muted = true;
|
||||
setTimeout(function() {
|
||||
var vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
vel.play();
|
||||
self.playElement(vel, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
@@ -460,12 +554,14 @@ MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
|
||||
|
||||
if (localVidEl && self.type == 'video') {
|
||||
localVidEl.autoplay = true;
|
||||
localVidEl.src = self.URL.createObjectURL(stream);
|
||||
this.assignElement(localVidEl,
|
||||
this.URL.createObjectURL(stream),
|
||||
"localVideo");
|
||||
localVidEl.muted = true;
|
||||
setTimeout(function() {
|
||||
var vel = self.getLocalVideoElement();
|
||||
if (vel.play) {
|
||||
vel.play();
|
||||
self.playElement(vel, "localVideo");
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
@@ -700,6 +796,7 @@ MatrixCall.prototype._onAddStream = function(event) {
|
||||
if (s.getVideoTracks().length > 0) {
|
||||
this.type = 'video';
|
||||
this.remoteAVStream = s;
|
||||
this.remoteAStream = s;
|
||||
} else {
|
||||
this.type = 'voice';
|
||||
this.remoteAStream = s;
|
||||
@@ -712,12 +809,20 @@ MatrixCall.prototype._onAddStream = function(event) {
|
||||
t.onstarted = hookCallback(self, self._onRemoteStreamTrackStarted);
|
||||
});
|
||||
|
||||
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
|
||||
if (event.stream.oninactive !== undefined) {
|
||||
event.stream.oninactive = hookCallback(self, self._onRemoteStreamEnded);
|
||||
}
|
||||
else {
|
||||
// onended is deprecated from Chrome 54
|
||||
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
|
||||
}
|
||||
|
||||
// not currently implemented in chrome
|
||||
event.stream.onstarted = hookCallback(self, self._onRemoteStreamStarted);
|
||||
|
||||
if (this.type === 'video') {
|
||||
_tryPlayRemoteStream(this);
|
||||
_tryPlayRemoteAudioStream(this);
|
||||
}
|
||||
else {
|
||||
_tryPlayRemoteAudioStream(this);
|
||||
@@ -824,21 +929,21 @@ var sendCandidate = function(self, content) {
|
||||
var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
|
||||
if (self.getRemoteVideoElement()) {
|
||||
if (self.getRemoteVideoElement().pause) {
|
||||
self.getRemoteVideoElement().pause();
|
||||
self.pauseElement(self.getRemoteVideoElement(), "remoteVideo");
|
||||
}
|
||||
self.getRemoteVideoElement().src = "";
|
||||
self.assignElement(self.getRemoteVideoElement(), "", "remoteVideo");
|
||||
}
|
||||
if (self.getRemoteAudioElement()) {
|
||||
if (self.getRemoteAudioElement().pause) {
|
||||
self.getRemoteAudioElement().pause();
|
||||
self.pauseElement(self.getRemoteAudioElement(), "remoteAudio");
|
||||
}
|
||||
self.getRemoteAudioElement().src = "";
|
||||
self.assignElement(self.getRemoteAudioElement(), "", "remoteAudio");
|
||||
}
|
||||
if (self.getLocalVideoElement()) {
|
||||
if (self.getLocalVideoElement().pause) {
|
||||
self.getLocalVideoElement().pause();
|
||||
self.pauseElement(self.getLocalVideoElement(), "localVideo");
|
||||
}
|
||||
self.getLocalVideoElement().src = "";
|
||||
self.assignElement(self.getLocalVideoElement(), "", "localVideo");
|
||||
}
|
||||
self.hangupParty = hangupParty;
|
||||
self.hangupReason = hangupReason;
|
||||
@@ -896,11 +1001,13 @@ var _tryPlayRemoteStream = function(self) {
|
||||
if (self.getRemoteVideoElement() && self.remoteAVStream) {
|
||||
var player = self.getRemoteVideoElement();
|
||||
player.autoplay = true;
|
||||
player.src = self.URL.createObjectURL(self.remoteAVStream);
|
||||
self.assignElement(player,
|
||||
self.URL.createObjectURL(self.remoteAVStream),
|
||||
"remoteVideo");
|
||||
setTimeout(function() {
|
||||
var vel = self.getRemoteVideoElement();
|
||||
if (vel.play) {
|
||||
vel.play();
|
||||
self.playElement(vel, "remoteVideo");
|
||||
}
|
||||
// OpenWebRTC does not support oniceconnectionstatechange yet
|
||||
if (self.webRtc.isOpenWebRTC()) {
|
||||
@@ -914,11 +1021,13 @@ var _tryPlayRemoteAudioStream = function(self) {
|
||||
if (self.getRemoteAudioElement() && self.remoteAStream) {
|
||||
var player = self.getRemoteAudioElement();
|
||||
player.autoplay = true;
|
||||
player.src = self.URL.createObjectURL(self.remoteAStream);
|
||||
self.assignElement(player,
|
||||
self.URL.createObjectURL(self.remoteAStream),
|
||||
"remoteAudio");
|
||||
setTimeout(function() {
|
||||
var ael = self.getRemoteAudioElement();
|
||||
if (ael.play) {
|
||||
ael.play();
|
||||
self.playElement(ael, "remoteAudio");
|
||||
}
|
||||
// OpenWebRTC does not support oniceconnectionstatechange yet
|
||||
if (self.webRtc.isOpenWebRTC()) {
|
||||
@@ -1102,6 +1211,7 @@ var forAllTracksOnStream = function(s, f) {
|
||||
/** The MatrixCall class. */
|
||||
module.exports.MatrixCall = MatrixCall;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new Matrix call for the browser.
|
||||
* @param {MatrixClient} client The client instance to use.
|
||||
|
||||
+1
-1
@@ -39,6 +39,6 @@
|
||||
"uglifyjs": "^2.4.10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"olm": "https://matrix.org/packages/npm/olm/olm-1.0.0.tgz"
|
||||
"olm": "https://matrix.org/packages/npm/olm/olm-1.3.0.tgz"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ function bobUploadsKeys() {
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
*/
|
||||
function aliQueryKeys() {
|
||||
function expectAliQueryKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobDeviceKeys).toBeDefined();
|
||||
|
||||
@@ -151,7 +151,7 @@ function aliQueryKeys() {
|
||||
*
|
||||
* @return {promise} which resolves once the http request has completed.
|
||||
*/
|
||||
function bobQueryKeys() {
|
||||
function expectBobQueryKeys() {
|
||||
// can't query keys before ali has uploaded them
|
||||
expect(aliDeviceKeys).toBeDefined();
|
||||
|
||||
@@ -166,6 +166,34 @@ function bobQueryKeys() {
|
||||
return bobHttpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
*/
|
||||
function expectAliClaimKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobOneTimeKeys).toBeDefined();
|
||||
|
||||
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
|
||||
for (var keyId in bobOneTimeKeys) {
|
||||
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
|
||||
return {one_time_keys: result};
|
||||
});
|
||||
|
||||
return aliHttpBackend.flush("/keys/claim", 1);
|
||||
}
|
||||
|
||||
|
||||
function aliDownloadsKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
@@ -180,7 +208,7 @@ function aliDownloadsKeys() {
|
||||
display_name: null,
|
||||
}]);
|
||||
});
|
||||
var p2 = aliQueryKeys();
|
||||
var p2 = expectAliQueryKeys();
|
||||
|
||||
// check that the localStorage is updated as we expect (not sure this is
|
||||
// an integration test, but meh)
|
||||
@@ -193,60 +221,91 @@ function aliDownloadsKeys() {
|
||||
}
|
||||
|
||||
function aliEnablesEncryption() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobOneTimeKeys).toBeDefined();
|
||||
|
||||
aliQueryKeys().catch(test_utils.failTest);
|
||||
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
|
||||
for (var keyId in bobOneTimeKeys) {
|
||||
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
|
||||
return {one_time_keys: result};
|
||||
});
|
||||
var p = aliClient.setRoomEncryption(roomId, {
|
||||
return aliClient.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
}).then(function(res) {
|
||||
expect(res[aliUserId]).toEqual({});
|
||||
expect(res[bobUserId][bobDeviceId].device).toBeDefined();
|
||||
expect(res[bobUserId][bobDeviceId].sessionId).toBeDefined();
|
||||
}).then(function() {
|
||||
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
});
|
||||
aliHttpBackend.flush();
|
||||
return p;
|
||||
}
|
||||
|
||||
function bobEnablesEncryption() {
|
||||
bobQueryKeys().catch(test_utils.failTest);
|
||||
return bobClient.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
}).then(function(res) {
|
||||
expect(res[aliUserId][aliDeviceId].device).toBeDefined();
|
||||
expect(res[aliUserId][aliDeviceId].sessionId).toBeDefined();
|
||||
expect(res[bobUserId]).toEqual({});
|
||||
expect(bobClient.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
}).then(function() {
|
||||
expect(bobClient.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ali sends a message, first claiming e2e keys. Set the expectations and
|
||||
* check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function aliSendsFirstMessage() {
|
||||
return q.all([
|
||||
sendMessage(aliClient),
|
||||
expectAliQueryKeys()
|
||||
.then(expectAliClaimKeys)
|
||||
.then(expectAliSendMessageRequest)
|
||||
]).spread(function(_, ciphertext) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ali sends a message without first claiming e2e keys. Set the expectations
|
||||
* and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function aliSendsMessage() {
|
||||
return sendMessage(aliHttpBackend, aliClient).then(function(content) {
|
||||
return q.all([
|
||||
sendMessage(aliClient),
|
||||
expectAliSendMessageRequest()
|
||||
]).spread(function(_, ciphertext) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
|
||||
* expectations and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Ali's device.
|
||||
*/
|
||||
function bobSendsReplyMessage() {
|
||||
return q.all([
|
||||
sendMessage(bobClient),
|
||||
expectBobQueryKeys()
|
||||
.then(expectBobSendMessageRequest)
|
||||
]).spread(function(_, ciphertext) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Ali will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function expectAliSendMessageRequest() {
|
||||
return expectSendMessageRequest(aliHttpBackend).then(function(content) {
|
||||
aliMessages.push(content);
|
||||
expect(utils.keys(content.ciphertext)).toEqual([bobDeviceCurve25519Key]);
|
||||
var ciphertext = content.ciphertext[bobDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeDefined();
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
|
||||
function bobSendsMessage() {
|
||||
return sendMessage(bobHttpBackend, bobClient).then(function(content) {
|
||||
/**
|
||||
* Set an expectation that Bob will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
function expectBobSendMessageRequest() {
|
||||
return expectSendMessageRequest(bobHttpBackend).then(function(content) {
|
||||
bobMessages.push(content);
|
||||
var aliKeyId = "curve25519:" + aliDeviceId;
|
||||
var aliDeviceCurve25519Key = aliDeviceKeys.keys[aliKeyId];
|
||||
@@ -257,7 +316,13 @@ function bobSendsMessage() {
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(httpBackend, client) {
|
||||
function sendMessage(client) {
|
||||
return client.sendMessage(
|
||||
roomId, {msgtype: "m.text", body: "Hello, World"}
|
||||
);
|
||||
}
|
||||
|
||||
function expectSendMessageRequest(httpBackend) {
|
||||
var path = "/send/m.room.encrypted/";
|
||||
var sent;
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
@@ -266,11 +331,7 @@ function sendMessage(httpBackend, client) {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
});
|
||||
var p1 = client.sendMessage(
|
||||
roomId, {msgtype: "m.text", body: "Hello, World"}
|
||||
);
|
||||
var p2 = httpBackend.flush(path, 1);
|
||||
return q.all([p1, p2]).then(function() {
|
||||
return httpBackend.flush(path, 1).then(function() {
|
||||
return sent;
|
||||
});
|
||||
}
|
||||
@@ -459,7 +520,7 @@ describe("MatrixClient crypto", function() {
|
||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||
|
||||
return q.all(aliClient.downloadKeys([bobUserId]),
|
||||
aliQueryKeys());
|
||||
expectAliQueryKeys());
|
||||
})
|
||||
.then(function() {
|
||||
// should get an empty list
|
||||
@@ -481,8 +542,8 @@ describe("MatrixClient crypto", function() {
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsMessage)
|
||||
.catch(test_utils.failTest).done(done);
|
||||
.then(aliSendsFirstMessage)
|
||||
.catch(test_utils.failTest).nodeify(done);
|
||||
});
|
||||
|
||||
it("Bob receives a message", function(done) {
|
||||
@@ -490,7 +551,7 @@ describe("MatrixClient crypto", function() {
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsMessage)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobStartClient)
|
||||
.then(bobRecvMessage)
|
||||
.catch(test_utils.failTest).done(done);
|
||||
@@ -501,13 +562,20 @@ describe("MatrixClient crypto", function() {
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliDownloadsKeys)
|
||||
.then(function() {
|
||||
aliClient.setDeviceBlocked(bobUserId, bobDeviceId, true);
|
||||
return sendMessage(aliHttpBackend, aliClient);
|
||||
}).then(function(sentContent) {
|
||||
// no unblocked devices, so the ciphertext should be empty
|
||||
expect(sentContent.ciphertext).toEqual({});
|
||||
}).catch(test_utils.failTest).done(done);
|
||||
var p1 = sendMessage(aliClient);
|
||||
var p2 = expectAliQueryKeys()
|
||||
.then(expectAliClaimKeys)
|
||||
.then(function() {
|
||||
return expectSendMessageRequest(aliHttpBackend);
|
||||
}).then(function(sentContent) {
|
||||
// no unblocked devices, so the ciphertext should be empty
|
||||
expect(sentContent.ciphertext).toEqual({});
|
||||
});
|
||||
return q.all([p1, p2]);
|
||||
}).catch(test_utils.failTest).nodeify(done);
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", function(done) {
|
||||
@@ -515,7 +583,7 @@ describe("MatrixClient crypto", function() {
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsMessage)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobStartClient)
|
||||
.then(bobRecvMessage)
|
||||
.then(aliSendsMessage)
|
||||
@@ -528,11 +596,11 @@ describe("MatrixClient crypto", function() {
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsMessage)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobStartClient)
|
||||
.then(bobRecvMessage)
|
||||
.then(bobEnablesEncryption)
|
||||
.then(bobSendsMessage).then(function(ciphertext) {
|
||||
.then(bobSendsReplyMessage).then(function(ciphertext) {
|
||||
expect(ciphertext.type).toEqual(1);
|
||||
}).then(aliRecvMessage)
|
||||
.catch(test_utils.failTest).done(done);
|
||||
|
||||
@@ -121,7 +121,8 @@ describe("getEventTimeline support", function() {
|
||||
startClient(httpBackend, client
|
||||
).then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
expect(function() { client.getEventTimeline(room, "event"); })
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() { client.getEventTimeline(timelineSet, "event"); })
|
||||
.toThrow();
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
@@ -137,7 +138,8 @@ describe("getEventTimeline support", function() {
|
||||
startClient(httpBackend, client
|
||||
).then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
expect(function() { client.getEventTimeline(room, "event"); })
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() { client.getEventTimeline(timelineSet, "event"); })
|
||||
.not.toThrow();
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
@@ -242,6 +244,7 @@ describe("MatrixClient event timelines", function() {
|
||||
describe("getEventTimeline", function() {
|
||||
it("should create a new timeline for new events", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
|
||||
.respond(200, function() {
|
||||
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);
|
||||
for (var i = 0; i < 4; 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) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
@@ -291,7 +295,7 @@ describe("MatrixClient event timelines", 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) {
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
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) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_4",
|
||||
rooms: {
|
||||
@@ -335,7 +340,7 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
client.on("sync", function() {
|
||||
client.getEventTimeline(room, EVENTS[2].event_id
|
||||
client.getEventTimeline(timelineSet, EVENTS[2].event_id
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
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",
|
||||
function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
||||
// with context which joins them all up.
|
||||
@@ -410,19 +416,19 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
var tl0, tl2, tl3;
|
||||
client.getEventTimeline(room, EVENTS[0].event_id
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl0 = tl;
|
||||
return client.getEventTimeline(room, EVENTS[2].event_id);
|
||||
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl2 = tl;
|
||||
return client.getEventTimeline(room, EVENTS[3].event_id);
|
||||
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
tl3 = tl;
|
||||
return client.getEventTimeline(room, EVENTS[1].event_id);
|
||||
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
|
||||
}).then(function(tl) {
|
||||
// we expect it to get merged in with event 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) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
|
||||
// with context which joins them all up.
|
||||
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) {
|
||||
// could do with a fail()
|
||||
expect(true).toBeFalsy();
|
||||
@@ -475,6 +482,7 @@ describe("MatrixClient event timelines", function() {
|
||||
describe("paginateEventTimeline", function() {
|
||||
it("should allow you to paginate backwards", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
@@ -503,7 +511,7 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
var tl;
|
||||
client.getEventTimeline(room, EVENTS[0].event_id
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(tl, {backwards: true});
|
||||
@@ -525,6 +533,7 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
it("should allow you to paginate forwards", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
|
||||
encodeURIComponent(EVENTS[0].event_id))
|
||||
@@ -553,7 +562,7 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
var tl;
|
||||
client.getEventTimeline(room, EVENTS[0].event_id
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(
|
||||
@@ -607,10 +616,11 @@ describe("MatrixClient event timelines", function() {
|
||||
|
||||
it("should work when /send returns before /sync", function(done) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
||||
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) {
|
||||
// 2 because the initial sync contained an event
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
@@ -619,7 +629,7 @@ describe("MatrixClient event timelines", function() {
|
||||
// now let the sync complete, and check it again
|
||||
return httpBackend.flush("/sync", 1);
|
||||
}).then(function() {
|
||||
return client.getEventTimeline(room, event.event_id);
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
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) {
|
||||
var room = client.getRoom(roomId);
|
||||
var timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
// 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.
|
||||
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
|
||||
console.log("sendTextMessage completed");
|
||||
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) {
|
||||
console.log("getEventTimeline completed (2)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
@@ -644,7 +655,7 @@ describe("MatrixClient event timelines", function() {
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
return client.getEventTimeline(room, event.event_id);
|
||||
return client.getEventTimeline(timelineSet, event.event_id);
|
||||
}).then(function(tl) {
|
||||
console.log("getEventTimeline completed (1)");
|
||||
expect(tl.getEvents().length).toEqual(2);
|
||||
@@ -655,4 +666,70 @@ describe("MatrixClient event timelines", function() {
|
||||
}).catch(utils.failTest);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should handle gappy syncs after redactions", function(done) {
|
||||
// https://github.com/vector-im/vector-web/issues/1389
|
||||
|
||||
// a state event, followed by a redaction thereof
|
||||
var event = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: otherUserId
|
||||
});
|
||||
var redaction = utils.mkEvent({
|
||||
type: "m.room.redaction",
|
||||
room_id: roomId,
|
||||
sender: otherUserId,
|
||||
content: {}
|
||||
});
|
||||
redaction.redacts = event.event_id;
|
||||
|
||||
var syncData = {
|
||||
next_batch: "batch1",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
redaction,
|
||||
],
|
||||
limited: false,
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
httpBackend.flush().then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
var tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
expect(tl.getEvents()[1].isRedacted()).toBe(true);
|
||||
|
||||
var sync2 = {
|
||||
next_batch: "batch2",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
sync2.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: roomId, user: otherUserId, msg: "world"
|
||||
}),
|
||||
],
|
||||
limited: true,
|
||||
prev_batch: "newerTok",
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, sync2);
|
||||
|
||||
return httpBackend.flush();
|
||||
}).then(function() {
|
||||
var room = client.getRoom(roomId);
|
||||
var tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
});
|
||||
|
||||
+6
-6
@@ -48,7 +48,7 @@ module.exports.mock = function(constr, name) {
|
||||
* @param {Object} opts Values for the event.
|
||||
* @param {string} opts.type The event.type
|
||||
* @param {string} opts.room The event.room_id
|
||||
* @param {string} opts.user The event.user_id
|
||||
* @param {string} opts.sender The event.sender
|
||||
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
|
||||
* @param {Object} opts.content The event.content
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
@@ -61,7 +61,7 @@ module.exports.mkEvent = function(opts) {
|
||||
var event = {
|
||||
type: opts.type,
|
||||
room_id: opts.room,
|
||||
sender: opts.user,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
event_id: "$" + Math.random() + "-" + Math.random()
|
||||
};
|
||||
@@ -88,7 +88,7 @@ module.exports.mkPresence = function(opts) {
|
||||
var event = {
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
type: "m.presence",
|
||||
sender: opts.user,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: {
|
||||
avatar_url: opts.url,
|
||||
displayname: opts.name,
|
||||
@@ -104,8 +104,8 @@ module.exports.mkPresence = function(opts) {
|
||||
* @param {Object} opts Values for the membership.
|
||||
* @param {string} opts.room The room ID for the event.
|
||||
* @param {string} opts.mship The content.membership for the event.
|
||||
* @param {string} opts.user The user ID for the event.
|
||||
* @param {string} opts.skey The other user ID for the event if applicable
|
||||
* @param {string} opts.sender The sender user ID for the event.
|
||||
* @param {string} opts.skey The target user ID for the event if applicable
|
||||
* e.g. for invites/bans.
|
||||
* @param {string} opts.name The content.displayname for the event.
|
||||
* @param {string} opts.url The content.avatar_url for the event.
|
||||
@@ -115,7 +115,7 @@ module.exports.mkPresence = function(opts) {
|
||||
module.exports.mkMembership = function(opts) {
|
||||
opts.type = "m.room.member";
|
||||
if (!opts.skey) {
|
||||
opts.skey = opts.user;
|
||||
opts.skey = opts.sender || opts.user;
|
||||
}
|
||||
if (!opts.mship) {
|
||||
throw new Error("Missing .mship => " + JSON.stringify(opts));
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
"use strict";
|
||||
var Crypto = require("../../lib/crypto");
|
||||
var sdk = require("../..");
|
||||
|
||||
describe("Crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
it("Crypto exposes the correct olm library version", function() {
|
||||
expect(Crypto.getOlmVersion()).toEqual([1, 3, 0]);
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,12 @@ describe("EventTimeline", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
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() {
|
||||
|
||||
+23
-21
@@ -479,14 +479,14 @@ describe("Room", function() {
|
||||
it("should handle events in the same timeline", function() {
|
||||
room.addLiveEvents(events);
|
||||
|
||||
expect(room.compareEventOrdering(events[0].getId(),
|
||||
events[1].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
|
||||
events[1].getId()))
|
||||
.toBeLessThan(0);
|
||||
expect(room.compareEventOrdering(events[2].getId(),
|
||||
events[1].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId(),
|
||||
events[1].getId()))
|
||||
.toBeGreaterThan(0);
|
||||
expect(room.compareEventOrdering(events[1].getId(),
|
||||
events[1].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
|
||||
events[1].getId()))
|
||||
.toEqual(0);
|
||||
});
|
||||
|
||||
@@ -498,11 +498,11 @@ describe("Room", function() {
|
||||
room.addEventsToTimeline([events[0]], false, oldTimeline);
|
||||
room.addLiveEvents([events[1]]);
|
||||
|
||||
expect(room.compareEventOrdering(events[0].getId(),
|
||||
events[1].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
|
||||
events[1].getId()))
|
||||
.toBeLessThan(0);
|
||||
expect(room.compareEventOrdering(events[1].getId(),
|
||||
events[0].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
|
||||
events[0].getId()))
|
||||
.toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -512,24 +512,26 @@ describe("Room", function() {
|
||||
room.addEventsToTimeline([events[0]], false, oldTimeline);
|
||||
room.addLiveEvents([events[1]]);
|
||||
|
||||
expect(room.compareEventOrdering(events[0].getId(),
|
||||
events[1].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
|
||||
events[1].getId()))
|
||||
.toBe(null);
|
||||
expect(room.compareEventOrdering(events[1].getId(),
|
||||
events[0].getId()))
|
||||
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
|
||||
events[0].getId()))
|
||||
.toBe(null);
|
||||
});
|
||||
|
||||
it("should return null for unknown events", function() {
|
||||
room.addLiveEvents(events);
|
||||
|
||||
expect(room.compareEventOrdering(events[0].getId(), "xxx"))
|
||||
.toBe(null);
|
||||
expect(room.compareEventOrdering("xxx", events[0].getId()))
|
||||
.toBe(null);
|
||||
expect(room.compareEventOrdering(events[0].getId(),
|
||||
events[0].getId()))
|
||||
.toBe(0);
|
||||
expect(room.getUnfilteredTimelineSet()
|
||||
.compareEventOrdering(events[0].getId(), "xxx"))
|
||||
.toBe(null);
|
||||
expect(room.getUnfilteredTimelineSet()
|
||||
.compareEventOrdering("xxx", events[0].getId()))
|
||||
.toBe(null);
|
||||
expect(room.getUnfilteredTimelineSet()
|
||||
.compareEventOrdering(events[0].getId(), events[0].getId()))
|
||||
.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,11 @@ function createTimeline(numEvents, baseIndex) {
|
||||
if (numEvents === undefined) { numEvents = 3; }
|
||||
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
|
||||
addEventsToTimeline(timeline, numEvents - baseIndex, false);
|
||||
@@ -133,19 +137,19 @@ describe("TimelineIndex", 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.
|
||||
*/
|
||||
var room, client;
|
||||
var timelineSet, client;
|
||||
function createWindow(timeline, opts) {
|
||||
room = {};
|
||||
timelineSet = {};
|
||||
client = {};
|
||||
client.getEventTimeline = function(room0, eventId0) {
|
||||
expect(room0).toBe(room);
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
return q(timeline);
|
||||
};
|
||||
|
||||
return new TimelineWindow(client, room, opts);
|
||||
return new TimelineWindow(client, timelineSet, opts);
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
@@ -169,15 +173,15 @@ describe("TimelineWindow", function() {
|
||||
var timeline = createTimeline();
|
||||
var eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
var room = {};
|
||||
var timelineSet = {};
|
||||
var client = {};
|
||||
client.getEventTimeline = function(room0, eventId0) {
|
||||
expect(room0).toBe(room);
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
expect(eventId0).toEqual(eventId);
|
||||
return q(timeline);
|
||||
};
|
||||
|
||||
var timelineWindow = new TimelineWindow(client, room);
|
||||
var timelineWindow = new TimelineWindow(client, timelineSet);
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
var expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
@@ -192,12 +196,12 @@ describe("TimelineWindow", function() {
|
||||
|
||||
var eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
var room = {};
|
||||
var timelineSet = {};
|
||||
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))
|
||||
.toBe(false);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
|
||||
Reference in New Issue
Block a user