diff --git a/lib/base-apis.js b/lib/base-apis.js index 656859a96..dc2c6899f 100644 --- a/lib/base-apis.js +++ b/lib/base-apis.js @@ -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.>} 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 */ diff --git a/lib/client.js b/lib/client.js index 086f3d77d..7a9271394 100644 --- a/lib/client.js +++ b/lib/client.js @@ -144,7 +144,6 @@ function MatrixClient(opts) { this._peekSync = null; this._isGuest = false; this._ongoingScrollbacks = {}; - this._txnCtr = 0; this.timelineSupport = Boolean(opts.timelineSupport); this.urlPreviewCache = {}; @@ -424,6 +423,12 @@ function setupCryptoEventHandler(client) { }); client.on("RoomMember.membership", client._crypto.onRoomMembership.bind(client._crypto)); + + client.on("toDeviceEvent", function(event) { + if (event.getType() == "m.room_key") { + client._crypto.onRoomKeyEvent(event); + } + }); } function onCryptoEvent(client, event) { @@ -438,20 +443,6 @@ function onCryptoEvent(client, event) { } } -/** - * 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. @@ -820,7 +811,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 @@ -921,11 +912,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; @@ -2663,13 +2656,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; diff --git a/lib/crypto-algorithms/megolm.js b/lib/crypto-algorithms/megolm.js index 5b27025a5..9b89a8da7 100644 --- a/lib/crypto-algorithms/megolm.js +++ b/lib/crypto-algorithms/megolm.js @@ -96,17 +96,15 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) { ).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 contentMap = {}; - var participantKeys = []; for (var userId in devicemap) { if (!devicemap.hasOwnProperty(userId)) { continue; } + contentMap[userId] = {}; + var devices = devicemap[userId]; for (var deviceId in devices) { @@ -115,29 +113,18 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) { } var deviceInfo = devices[deviceId].device; - participantKeys.push(deviceInfo.getIdentityKey()); + contentMap[userId][deviceId] = + olmlib.encryptMessageForDevices( + self._deviceId, + self._olmDevice, + [deviceInfo.getIdentityKey()], + payload + ); } } - 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 - ); + return self._baseApis.sendToDevice("m.room.encrypted", contentMap); }).then(function() { if (self._discardNewSession) { // we've had cause to reset the session_id since starting this process. diff --git a/lib/sync.js b/lib/sync.js index 91bb0cc51..64f8ae504 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -496,36 +496,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 +555,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 +625,17 @@ 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) { + 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)