You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Merge pull request #1684 from matrix-org/gsouquet/cache-decrypt
This commit is contained in:
@@ -484,8 +484,9 @@ describe("megolm", function() {
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(function() {
|
||||
}).then(async function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
await room.decryptCriticalEvents();
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
@@ -933,8 +934,9 @@ describe("megolm", function() {
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
}).then(async function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
await room.decryptCriticalEvents();
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
|
||||
|
||||
@@ -212,10 +212,12 @@ MockStorageApi.prototype = {
|
||||
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
|
||||
*/
|
||||
export function awaitDecryption(event) {
|
||||
if (!event.isBeingDecrypted()) {
|
||||
return Promise.resolve(event);
|
||||
}
|
||||
|
||||
// An event is not always decrypted ahead of time
|
||||
// getClearContent is a good signal to know whether an event has been decrypted
|
||||
// already
|
||||
if (event.getClearContent() !== null) {
|
||||
return event;
|
||||
} else {
|
||||
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -225,6 +227,7 @@ export function awaitDecryption(event) {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function HttpResponse(
|
||||
|
||||
@@ -5554,8 +5554,9 @@ function _resolve(callback, resolve, res) {
|
||||
resolve(res);
|
||||
}
|
||||
|
||||
function _PojoToMatrixEventMapper(client, options) {
|
||||
const preventReEmit = Boolean(options && options.preventReEmit);
|
||||
function _PojoToMatrixEventMapper(client, options = {}) {
|
||||
const preventReEmit = Boolean(options.preventReEmit);
|
||||
const decrypt = options.decrypt !== false;
|
||||
function mapper(plainOldJsObject) {
|
||||
const event = new MatrixEvent(plainOldJsObject);
|
||||
if (event.isEncrypted()) {
|
||||
@@ -5564,8 +5565,10 @@ function _PojoToMatrixEventMapper(client, options) {
|
||||
"Event.decrypted",
|
||||
]);
|
||||
}
|
||||
if (decrypt) {
|
||||
event.attemptDecryption(client._crypto);
|
||||
}
|
||||
}
|
||||
if (!preventReEmit) {
|
||||
client.reEmitter.reEmit(event, ["Event.replaced"]);
|
||||
}
|
||||
@@ -5577,6 +5580,7 @@ function _PojoToMatrixEventMapper(client, options) {
|
||||
/**
|
||||
* @param {object} [options]
|
||||
* @param {bool} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client
|
||||
* @param {bool} options.decrypt decrypt event proactively
|
||||
* @return {Function}
|
||||
*/
|
||||
MatrixClient.prototype.getEventMapper = function(options = undefined) {
|
||||
|
||||
@@ -1692,7 +1692,7 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
|
||||
|
||||
await Promise.all([...pending].map(async (ev) => {
|
||||
try {
|
||||
await ev.attemptDecryption(this._crypto, true);
|
||||
await ev.attemptDecryption(this._crypto, { isRetry: true });
|
||||
} catch (e) {
|
||||
// don't die if something goes wrong
|
||||
}
|
||||
|
||||
@@ -399,6 +399,12 @@ utils.extend(MatrixEvent.prototype, {
|
||||
this._clearEvent.content.msgtype === "m.bad.encrypted";
|
||||
},
|
||||
|
||||
shouldAttemptDecryption: function() {
|
||||
return this.isEncrypted()
|
||||
&& !this.isBeingDecrypted()
|
||||
&& this.getClearContent() === null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Start the process of trying to decrypt this event.
|
||||
*
|
||||
@@ -407,12 +413,22 @@ utils.extend(MatrixEvent.prototype, {
|
||||
* @internal
|
||||
*
|
||||
* @param {module:crypto} crypto crypto module
|
||||
* @param {bool} isRetry True if this is a retry (enables more logging)
|
||||
* @param {object} options
|
||||
* @param {bool} options.isRetry True if this is a retry (enables more logging)
|
||||
* @param {bool} options.emit Emits "event.decrypted" if set to true
|
||||
*
|
||||
* @returns {Promise} promise which resolves (to undefined) when the decryption
|
||||
* attempt is completed.
|
||||
*/
|
||||
attemptDecryption: async function(crypto, isRetry) {
|
||||
attemptDecryption: async function(crypto, options = {}) {
|
||||
// For backwards compatibility purposes
|
||||
// The function signature used to be attemptDecryption(crypto, isRetry)
|
||||
if (typeof options === "boolean") {
|
||||
options = {
|
||||
isRetry: options,
|
||||
};
|
||||
}
|
||||
|
||||
// start with a couple of sanity checks.
|
||||
if (!this.isEncrypted()) {
|
||||
throw new Error("Attempt to decrypt event which isn't encrypted");
|
||||
@@ -442,7 +458,7 @@ utils.extend(MatrixEvent.prototype, {
|
||||
return this._decryptionPromise;
|
||||
}
|
||||
|
||||
this._decryptionPromise = this._decryptionLoop(crypto, isRetry);
|
||||
this._decryptionPromise = this._decryptionLoop(crypto, options);
|
||||
return this._decryptionPromise;
|
||||
},
|
||||
|
||||
@@ -487,7 +503,7 @@ utils.extend(MatrixEvent.prototype, {
|
||||
return recipients;
|
||||
},
|
||||
|
||||
_decryptionLoop: async function(crypto, isRetry) {
|
||||
_decryptionLoop: async function(crypto, options = {}) {
|
||||
// make sure that this method never runs completely synchronously.
|
||||
// (doing so would mean that we would clear _decryptionPromise *before*
|
||||
// it is set in attemptDecryption - and hence end up with a stuck
|
||||
@@ -504,7 +520,7 @@ utils.extend(MatrixEvent.prototype, {
|
||||
res = this._badEncryptedMessage("Encryption not enabled");
|
||||
} else {
|
||||
res = await crypto.decryptEvent(this);
|
||||
if (isRetry) {
|
||||
if (options.isRetry === true) {
|
||||
logger.info(`Decrypted event on retry (id=${this.getId()})`);
|
||||
}
|
||||
}
|
||||
@@ -512,7 +528,7 @@ utils.extend(MatrixEvent.prototype, {
|
||||
if (e.name !== "DecryptionError") {
|
||||
// not a decryption error: log the whole exception as an error
|
||||
// (and don't bother with a retry)
|
||||
const re = isRetry ? 're' : '';
|
||||
const re = options.isRetry ? 're' : '';
|
||||
logger.error(
|
||||
`Error ${re}decrypting event ` +
|
||||
`(id=${this.getId()}): ${e.stack || e}`,
|
||||
@@ -578,7 +594,9 @@ utils.extend(MatrixEvent.prototype, {
|
||||
// pick up the wrong contents.
|
||||
this.setPushActions(null);
|
||||
|
||||
if (options.emit !== false) {
|
||||
this.emit("Event.decrypted", this, err);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -228,6 +228,51 @@ function pendingEventsKey(roomId) {
|
||||
|
||||
utils.inherits(Room, EventEmitter);
|
||||
|
||||
|
||||
/**
|
||||
* Bulk decrypt critical events in a room
|
||||
*
|
||||
* Critical events represents the minimal set of events to decrypt
|
||||
* for a typical UI to function properly
|
||||
*
|
||||
* - Last event of every room (to generate likely message preview)
|
||||
* - All events up to the read receipt (to calculate an accurate notification count)
|
||||
*
|
||||
* @returns {Promise} Signals when all events have been decrypted
|
||||
*/
|
||||
Room.prototype.decryptCriticalEvents = function() {
|
||||
const readReceiptEventId = this.getEventReadUpTo(this._client.getUserId(), true);
|
||||
const events = this.getLiveTimeline().getEvents();
|
||||
const readReceiptTimelineIndex = events.findIndex(matrixEvent => {
|
||||
return matrixEvent.event.event_id === readReceiptEventId;
|
||||
});
|
||||
|
||||
const decryptionPromises = events
|
||||
.slice(readReceiptTimelineIndex)
|
||||
.filter(event => event.shouldAttemptDecryption())
|
||||
.reverse()
|
||||
.map(event => event.attemptDecryption(this._client._crypto, { isRetry: true }));
|
||||
|
||||
return Promise.allSettled(decryptionPromises);
|
||||
};
|
||||
|
||||
/**
|
||||
* Bulk decrypt events in a room
|
||||
*
|
||||
* @returns {Promise} Signals when all events have been decrypted
|
||||
*/
|
||||
Room.prototype.decryptAllEvents = function() {
|
||||
const decryptionPromises = this
|
||||
.getUnfilteredTimelineSet()
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
.filter(event => event.shouldAttemptDecryption())
|
||||
.reverse()
|
||||
.map(event => event.attemptDecryption(this._client._crypto, { isRetry: true }));
|
||||
|
||||
return Promise.allSettled(decryptionPromises);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the version of the room
|
||||
* @returns {string} The version of the room, or null if it could not be determined
|
||||
|
||||
18
src/sync.js
18
src/sync.js
@@ -1158,10 +1158,15 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
await utils.promiseMapSeries(joinRooms, async function(joinObj) {
|
||||
const room = joinObj.room;
|
||||
const stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
|
||||
const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room);
|
||||
// Prevent events from being decrypted ahead of time
|
||||
// this helps large account to speed up faster
|
||||
// room::decryptCriticalEvent is in charge of decrypting all the events
|
||||
// required for a client to function properly
|
||||
const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room, false);
|
||||
const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral);
|
||||
const accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data);
|
||||
|
||||
const encrypted = client.isRoomEncrypted(room.roomId);
|
||||
// we do this first so it's correct when any of the events fire
|
||||
if (joinObj.unread_notifications) {
|
||||
room.setUnreadNotificationCount(
|
||||
@@ -1172,7 +1177,6 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
// bother setting it here. We trust our calculations better than the
|
||||
// server's for this case, and therefore will assume that our non-zero
|
||||
// count is accurate.
|
||||
const encrypted = client.isRoomEncrypted(room.roomId);
|
||||
if (!encrypted
|
||||
|| (encrypted && room.getUnreadNotificationCount('highlight') <= 0)) {
|
||||
room.setUnreadNotificationCount(
|
||||
@@ -1294,6 +1298,11 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
});
|
||||
|
||||
room.updateMyMembership("join");
|
||||
|
||||
// Decrypt only the last message in all rooms to make sure we can generate a preview
|
||||
// And decrypt all events after the recorded read receipt to ensure an accurate
|
||||
// notification count
|
||||
room.decryptCriticalEvents();
|
||||
});
|
||||
|
||||
// Handle leaves (e.g. kicked rooms)
|
||||
@@ -1516,13 +1525,14 @@ SyncApi.prototype._mapSyncResponseToRoomArray = function(obj) {
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @param {Room} room
|
||||
* @param {bool} decrypt
|
||||
* @return {MatrixEvent[]}
|
||||
*/
|
||||
SyncApi.prototype._mapSyncEventsFormat = function(obj, room) {
|
||||
SyncApi.prototype._mapSyncEventsFormat = function(obj, room, decrypt = true) {
|
||||
if (!obj || !utils.isArray(obj.events)) {
|
||||
return [];
|
||||
}
|
||||
const mapper = this.client.getEventMapper();
|
||||
const mapper = this.client.getEventMapper({ decrypt });
|
||||
return obj.events.map(function(e) {
|
||||
if (room) {
|
||||
e.room_id = room.roomId;
|
||||
|
||||
Reference in New Issue
Block a user