1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-28 05:03:59 +03:00

Factor Olm encryption/decryption out to new classes

- to make way for alternative encryption algorithms. We now store an encryption
object for each room, rather than referring to sessionstore on each event.

Also a little light tidying to the jsdocs.
This commit is contained in:
Richard van der Hoff
2016-08-16 14:29:15 +01:00
parent 4cde51b3ce
commit 2c9f8ba598
9 changed files with 455 additions and 179 deletions

View File

@@ -15,6 +15,12 @@ limitations under the License.
*/
"use strict";
/**
* olm.js wrapper
*
* @module OlmDevice
*/
var Olm = require("olm");
var utils = require("./utils");
@@ -25,6 +31,8 @@ var utils = require("./utils");
* Accounts and sessions are kept pickled in a sessionStore.
*
* @constructor
* @alias module:OlmDevice
*
* @param {Object} sessionStore A store to be used for data in end-to-end
* crypto
*

View File

@@ -2315,7 +2315,7 @@ MatrixClient.prototype.getTurnServers = function() {
* appear in a room's timeline. If "<b>chronological</b>", messages will appear
* in the timeline when the call to <code>sendEvent</code> was made. If
* "<b>detached</b>", pending messages will appear in a separate list,
* accessbile via {@link module:models/room~Room#getPendingEvents}. Default:
* accessbile via {@link module:models/room#getPendingEvents}. Default:
* "chronological".
*
* @param {Number=} opts.pollTimeout The number of milliseconds to wait on /events.

View File

@@ -0,0 +1,137 @@
/*
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";
/**
* Internal module. Defines the base classes of the encryption implementations
*
* @module crypto-algorithms/base
*/
var utils = require("../utils");
/**
* map of registered encryption algorithm classes. A map from string to {@link
* module:crypto-algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class
*
* @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
*
* @type {Object.<string, function(new: module:crypto-algorithms/base.DecryptionAlgorithm)>}
*/
module.exports.DECRYPTION_CLASSES = {};
/**
* base type for encryption implementations
*
* @constructor
*
* @param {string} deviceId The identifier for this device.
* @param {module:crypto} crypto crypto core
* @param {module:OlmDevice} olmDevice olm.js wrapper
*/
module.exports.EncryptionAlgorithm = function(deviceId, crypto, olmDevice) {
this._deviceId = deviceId;
this._crypto = crypto;
this._olmDevice = olmDevice;
};
/**
* Initialise this EncryptionAlgorithm instance for a particular room
*
* @method module:crypto-algorithms/base.EncryptionAlgorithm#initRoomEncryption
* @abstract
*
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {module:client.Promise} Promise which resolves when setup is complete
*/
/**
* Encrypt a message event
*
* @method module:crypto-algorithms/base.EncryptionAlgorithm#encryptMessage
* @abstract
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {object} new event body
*/
/**
* base type for decryption implementations
*
* @constructor
* @param {string} deviceId The identifier for this device.
* @param {module:crypto} crypto crypto core
* @param {module:OlmDevice} olmDevice olm.js wrapper
*/
module.exports.DecryptionAlgorithm = function(deviceId, crypto, olmDevice) {
this._deviceId = deviceId;
this._crypto = crypto;
this._olmDevice = olmDevice;
};
/**
* Decrypt an event
*
* @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
* problem decrypting the event
*/
/**
* Exception thrown when decryption fails
*
* @constructor
* @param {string} msg message describing the problem
* @extends Error
*/
module.exports.DecryptionError = function(msg) {
this.message = msg;
};
utils.inherits(module.exports.DecryptionError, Error);
/**
* Registers an encryption/decryption class for a particular algorithm
*
* @param {string} algorithm algorithm tag to register for
*
* @param {class} encryptor {@link
* module:crypto-algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm}
* implementation
*
* @param {class} decryptor {@link
* module:crypto-algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
* implementation
*/
module.exports.registerAlgorithm = function(algorithm, encryptor, decryptor) {
module.exports.ENCRYPTION_CLASSES[algorithm] = encryptor;
module.exports.DECRYPTION_CLASSES[algorithm] = decryptor;
};

View File

@@ -0,0 +1,39 @@
/*
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 crypto-algorithms
*/
var base = require("./base");
require("./olm");
/**
* @see module:crypto-algorithms/base.ENCRYPTION_CLASSES
*/
module.exports.ENCRYPTION_CLASSES = base.ENCRYPTION_CLASSES;
/**
* @see module:crypto-algorithms/base.DECRYPTION_CLASSES
*/
module.exports.DECRYPTION_CLASSES = base.DECRYPTION_CLASSES;
/**
* @see module:crypto-algorithms/base.DecryptionError
*/
module.exports.DecryptionError = base.DecryptionError;

View File

@@ -0,0 +1,218 @@
/*
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";
/**
* Defines m.olm encryption/decryption
*
* @module crypto-algorithms/olm
*/
var utils = require("../utils");
var base = require("./base");
var OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
/**
* Olm encryption implementation
*
* @constructor
* @extends {module:crypto-algorithms/base.EncryptionAlgorithm}
*
* @param {string} deviceId The identifier for this device.
* @param {module:crypto} crypto crypto core
* @param {module:OlmDevice} olmDevice olm.js wrapper
*
*/
function OlmEncryption(deviceId, crypto, olmDevice) {
base.EncryptionAlgorithm.call(this, deviceId, crypto, olmDevice);
}
utils.inherits(OlmEncryption, base.EncryptionAlgorithm);
/**
* @inheritdoc
* @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);
});
};
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {object} new event body
*/
OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
if (!room) {
throw new Error("Cannot send encrypted messages in unknown rooms");
}
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up
// just as you are sending a secret message?
var users = utils.map(room.getJoinedMembers(), function(u) {
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 dev = devices[j];
if (dev.blocked) {
continue;
}
for (var keyId in dev.keys) {
if (keyId.indexOf("curve25519:") === 0) {
participantKeys.push(dev.keys[keyId]);
}
}
}
}
participantKeys.sort();
var participantHash = ""; // Olm.sha256(participantKeys.join());
var payloadJson = {
room_id: room.roomId,
type: eventType,
fingerprint: participantHash,
sender_device: this._deviceId,
content: content
};
var ciphertext = {};
var payloadString = JSON.stringify(payloadJson);
for (i = 0; i < participantKeys.length; ++i) {
var deviceKey = participantKeys[i];
if (deviceKey == this._olmDevice.deviceCurve25519Key) {
continue;
}
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
// Use the session with the lowest ID.
sessionIds.sort();
if (sessionIds.length === 0) {
// If we don't have a session for a device then
// we can't encrypt a message for it.
continue;
}
var sessionId = sessionIds[0];
console.log("Using sessionid " + sessionId + " for device " + deviceKey);
ciphertext[deviceKey] = this._olmDevice.encryptMessage(
deviceKey, sessionId, payloadString
);
}
var encryptedContent = {
algorithm: OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext
};
return encryptedContent;
};
/**
* Olm decryption implementation
*
* @constructor
* @extends {module:crypto-algorithms/base.DecryptionAlgorithm}
*
* @param {string} deviceId The identifier for this device.
* @param {module:crypto} crypto crypto core
* @param {module:OlmDevice} olmDevice olm.js wrapper
*
*/
function OlmDecryption(deviceId, crypto, olmDevice) {
base.DecryptionAlgorithm.call(this, deviceId, crypto, olmDevice);
}
utils.inherits(OlmDecryption, 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
*/
OlmDecryption.prototype.decryptEvent = function(event) {
var content = event.content;
var deviceKey = content.sender_key;
var ciphertext = content.ciphertext;
if (!ciphertext) {
throw new base.DecryptionError("Missing ciphertext");
}
if (!(this._olmDevice.deviceCurve25519Key in content.ciphertext)) {
throw new base.DecryptionError("Not included in recipients");
}
var message = content.ciphertext[this._olmDevice.deviceCurve25519Key];
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
var payloadString = null;
var foundSession = false;
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
var res = this._olmDevice.decryptMessage(
deviceKey, sessionId, message.type, message.body
);
payloadString = res.payload;
if (payloadString) {
console.log("decrypted with sessionId " + sessionId);
break;
}
if (res.matchesInbound) {
// this was a prekey message which matched this session; don't
// create a new session.
foundSession = true;
break;
}
}
if (message.type === 0 && !foundSession && payloadString === null) {
try {
payloadString = this._olmDevice.createInboundSession(
deviceKey, message.type, message.body
);
console.log("created new inbound sesion");
} catch (e) {
// Failed to decrypt with a new session.
}
}
// TODO: Check the sender user id matches the sender key.
// TODO: check the room_id and fingerprint
if (payloadString !== null) {
return JSON.parse(payloadString);
} else {
throw new base.DecryptionError("Bad Encrypted Message");
}
};
base.registerAlgorithm(OLM_ALGORITHM, OlmEncryption, OlmDecryption);

View File

@@ -23,11 +23,15 @@ limitations under the License.
var anotherjson = require('another-json');
var q = require("q");
var utils = require("./utils");
var OlmDevice = require("./OlmDevice");
var algorithms = require("./crypto-algorithms");
var OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
/**
* @enum
*/
var DeviceVerification = {
VERIFIED: 1,
UNVERIFIED: 0,
@@ -46,7 +50,7 @@ var DeviceVerification = {
* @property {Object.<string,string>} keys a map from
* &lt;key type&gt;:&lt;id&gt; -> &lt;base64-encoded key&gt;>
*
* @property {DeviceVerification} verified whether the device has been
* @property {module:crypto~DeviceVerification} verified whether the device has been
* verified by the user
*
* @property {Object} unsigned additional data from the homeserver
@@ -119,6 +123,7 @@ DeviceInfo.prototype.getDisplayname = function() {
/**
* Cryptography bits
*
* @alias module:crypto.Crypto
* @constructor
* @alias module:crypto
*
@@ -137,10 +142,10 @@ function Crypto(baseApis, sessionStore, userId, deviceId) {
this._userId = userId;
this._deviceId = deviceId;
this._cryptoAlgorithms = [];
this._olmDevice = new OlmDevice(sessionStore);
this._cryptoAlgorithms = [OLM_ALGORITHM];
// EncryptionAlgorithm instance for each room
this._roomAlgorithms = {};
// build our device keys: these will later be uploaded
this._deviceKeys = {};
@@ -152,7 +157,7 @@ function Crypto(baseApis, sessionStore, userId, deviceId) {
// add our own deviceinfo to the sessionstore
var deviceInfo = {
keys: this._deviceKeys,
algorithms: this._cryptoAlgorithms,
algorithms: [OLM_ALGORITHM],
verified: DeviceVerification.VERIFIED,
};
var myDevices = this._sessionStore.getEndToEndDevicesForUser(
@@ -205,7 +210,7 @@ function _uploadDeviceKeys(crypto) {
var deviceId = crypto._deviceId;
var deviceKeys = {
algorithms: crypto._cryptoAlgorithms,
algorithms: [OLM_ALGORITHM],
device_id: deviceId,
keys: crypto._deviceKeys,
user_id: userId,
@@ -388,7 +393,6 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
delete deviceResult.signatures;
var json = anotherjson.stringify(deviceResult);
console.log("msg:", json);
try {
_olmDevice.verifySignature(signKey, json, signature);
} catch (e) {
@@ -599,8 +603,6 @@ Crypto.prototype.isSenderKeyVerified = function(userId, algorithm, sender_key) {
* @return {Object} A promise that will resolve when encryption is setup.
*/
Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
var self = this;
// 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);
@@ -608,23 +610,24 @@ 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;
return q();
}
}
if (config.algorithm !== OLM_ALGORITHM) {
throw new Error("Unknown algorithm: " + config.algorithm);
var AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm];
if (!AlgClass) {
throw new Error("Unable to encrypt with " + config.algorithm);
}
// remove spurious keys
config = {
algorithm: OLM_ALGORITHM,
algorithm: config.algorithm,
};
this._sessionStore.storeEndToEndRoom(roomId, config);
return self.downloadKeys(roomMembers, true).then(function(res) {
return self._ensureOlmSessionsForUsers(roomMembers);
});
var alg = new AlgClass(this._deviceId, this, this._olmDevice);
this._roomAlgorithms[roomId] = alg;
return alg.initRoomEncryption(roomMembers);
};
/**
@@ -636,10 +639,8 @@ Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
* an object with keys <tt>missingUsers</tt> (a list of users with no known
* olm devices), and <tt>missingDevices</tt> a list of olm devices with no
* known one-time keys.
*
* @private
*/
Crypto.prototype._ensureOlmSessionsForUsers = function(users) {
Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
var devicesWithoutSession = [];
var userWithoutDevices = [];
for (var i = 0; i < users.length; ++i) {
@@ -710,7 +711,7 @@ Crypto.prototype._ensureOlmSessionsForUsers = function(users) {
* @return {bool} whether encryption is enabled.
*/
Crypto.prototype.isRoomEncrypted = function(roomId) {
return (this._sessionStore.getEndToEndRoom(roomId) && true) || false;
return Boolean(this._roomAlgorithms[roomId]);
};
@@ -718,7 +719,7 @@ Crypto.prototype.isRoomEncrypted = function(roomId) {
* Encrypt an event according to the configuration of the room, if necessary.
*
* @param {module:models/event.MatrixEvent} event event to be sent
* @param {module:models/room.Room} room destination room
* @param {module:models/room} room destination room
*/
Crypto.prototype.encryptEventIfNeeded = function(event, room) {
if (event.isEncrypted()) {
@@ -735,112 +736,27 @@ Crypto.prototype.encryptEventIfNeeded = function(event, room) {
var roomId = event.getRoomId();
var e2eRoomInfo = this._sessionStore.getEndToEndRoom(roomId);
if (!e2eRoomInfo || !e2eRoomInfo.algorithm) {
var alg = this._roomAlgorithms[roomId];
if (!alg) {
// not encrypting messages in this room
// check that the HS hasn't hidden the crypto event
if (this._sessionStore.getEndToEndRoom(roomId)) {
throw new Error(
"Room was previously configured to use encryption, but is " +
"no longer. Perhaps the homeserver is hiding the " +
"configuration event."
);
}
return;
}
var encryptedContent = this._encryptMessage(
room, e2eRoomInfo, event.getType(), event.getContent()
var encryptedContent = alg.encryptMessage(
room, event.getType(), event.getContent()
);
event.makeEncrypted("m.room.encrypted", encryptedContent);
};
/**
*
* @param {module:models/room.Room} room
* @param {object} e2eRoomInfo
* @param {string} eventType
* @param {object} content
*
* @return {object} new event body
*
* @private
*/
Crypto.prototype._encryptMessage = function(room, e2eRoomInfo, eventType, content) {
if (e2eRoomInfo.algorithm !== OLM_ALGORITHM) {
throw new Error("Unknown end-to-end algorithm: " + e2eRoomInfo.algorithm);
}
if (!room) {
throw new Error("Cannot send encrypted messages in unknown rooms");
}
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up
// just as you are sending a secret message?
var users = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
var participantKeys = [];
for (var i = 0; i < users.length; ++i) {
var userId = users[i];
var devices = this.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var dev = devices[j];
if (dev.blocked) {
continue;
}
for (var keyId in dev.keys) {
if (keyId.indexOf("curve25519:") === 0) {
participantKeys.push(dev.keys[keyId]);
}
}
}
}
participantKeys.sort();
var participantHash = ""; // Olm.sha256(participantKeys.join());
var payloadJson = {
room_id: room.roomId,
type: eventType,
fingerprint: participantHash,
sender_device: this._deviceId,
content: content
};
var ciphertext = {};
var payloadString = JSON.stringify(payloadJson);
for (i = 0; i < participantKeys.length; ++i) {
var deviceKey = participantKeys[i];
if (deviceKey == this._olmDevice.deviceCurve25519Key) {
continue;
}
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
// Use the session with the lowest ID.
sessionIds.sort();
if (sessionIds.length === 0) {
// If we don't have a session for a device then
// we can't encrypt a message for it.
continue;
}
var sessionId = sessionIds[0];
console.log("Using sessionid " + sessionId + " for device " + deviceKey);
ciphertext[deviceKey] = this._olmDevice.encryptMessage(
deviceKey, sessionId, payloadString
);
}
var encryptedContent = {
algorithm: e2eRoomInfo.algorithm,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext
};
return encryptedContent;
};
function DecryptionError(msg) {
this.message = msg;
}
utils.inherits(DecryptionError, Error);
/**
* Exception thrown when decryption fails
*/
Crypto.DecryptionError = DecryptionError;
/**
* Decrypt a received event
*
@@ -848,66 +764,23 @@ Crypto.DecryptionError = DecryptionError;
*
* @return {object} decrypted payload (with properties 'type', 'content')
*
* @raises {DecryptionError} if there is a problem decrypting the event
* @raises {algorithms.DecryptionError} if there is a problem decrypting the event
*/
Crypto.prototype.decryptEvent = function(event) {
var content = event.content;
if (content.algorithm !== OLM_ALGORITHM) {
throw new DecryptionError("Unknown algorithm");
}
var deviceKey = content.sender_key;
var ciphertext = content.ciphertext;
if (!ciphertext) {
throw new DecryptionError("Missing ciphertext");
}
if (!(this._olmDevice.deviceCurve25519Key in content.ciphertext)) {
throw new DecryptionError("Not included in recipients");
}
var message = content.ciphertext[this._olmDevice.deviceCurve25519Key];
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
var payloadString = null;
var foundSession = false;
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
var res = this._olmDevice.decryptMessage(
deviceKey, sessionId, message.type, message.body
);
payloadString = res.payload;
if (payloadString) {
console.log("decrypted with sessionId " + sessionId);
break;
}
if (res.matchesInbound) {
// this was a prekey message which matched this session; don't
// create a new session.
foundSession = true;
break;
}
}
if (message.type === 0 && !foundSession && payloadString === null) {
try {
payloadString = this._olmDevice.createInboundSession(
deviceKey, message.type, message.body
);
console.log("created new inbound sesion");
} catch (e) {
// Failed to decrypt with a new session.
}
}
// TODO: Check the sender user id matches the sender key.
if (payloadString !== null) {
return JSON.parse(payloadString);
} else {
throw new DecryptionError("Bad Encrypted Message");
var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
if (!AlgClass) {
throw new algorithms.DecryptionError("Unable to decrypt " + content.algorithm);
}
var alg = new AlgClass(this._deviceId, this, this._olmDevice);
return alg.decryptEvent(event);
};
/**
* @see module:crypto-algorithms/base.DecryptionError
*/
Crypto.DecryptionError = algorithms.DecryptionError;
/** */
module.exports = Crypto;

View File

@@ -30,7 +30,7 @@ module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
module.exports.MatrixError = require("./http-api").MatrixError;
/** The {@link module:client.MatrixClient|MatrixClient} class. */
module.exports.MatrixClient = require("./client").MatrixClient;
/** The {@link module:models/room~Room|Room} class. */
/** The {@link module:models/room|Room} class. */
module.exports.Room = require("./models/room");
/** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline");

View File

@@ -75,6 +75,7 @@ function synthesizeReceipt(userId, event, receiptType) {
* map from event_id to timeline and index.
*
* @constructor
* @alias module:models/room
* @param {string} roomId Required. The ID of this room.
* @param {Object=} opts Configuration options
* @param {*} opts.storageToken Optional. The token which a data store can use
@@ -85,7 +86,7 @@ function synthesizeReceipt(userId, event, receiptType) {
* appear in a room's timeline. If "<b>chronological</b>", messages will appear
* in the timeline when the call to <code>sendEvent</code> was made. If
* "<b>detached</b>", pending messages will appear in a separate list,
* accessbile via {@link module:models/room~Room#getPendingEvents}. Default:
* accessbile via {@link module:models/room#getPendingEvents}. Default:
* "chronological".
*
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved

View File

@@ -41,7 +41,7 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
* Construct a TimelineWindow.
*
* <p>This abstracts the separate timelines in a Matrix {@link
* module:models/room~Room|Room} into a single iterable thing. It keeps track of
* module:models/room|Room} into a single iterable thing. It keeps track of
* the start and endpoints of the window, which can be advanced with the help
* of pagination requests.
*