From 241176d6fb6abf35898812663d33490b953c22e9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 22 Sep 2020 18:22:37 +0200 Subject: [PATCH 01/12] WIP --- src/matrix/room/Room.js | 9 +++ src/matrix/room/RoomSummary.js | 25 ++++++- .../room/timeline/entries/EventEntry.js | 4 + .../timeline/persistence/TimelineReader.js | 73 ++++++++++--------- 4 files changed, 74 insertions(+), 37 deletions(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 99c2155e..2b9c3355 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -52,6 +52,15 @@ export class Room extends EventEmitter { this._clock = clock; } + _readEntriesToRetryDecryption(retryEventIds) { + const readFromEventKey = retryEventIds.length !== 0; + const stores = + const txn = await this._storage.readTxn([ + this._storage.storeNames.timelineEvents, + this._storage.storeNames.inboundGroupSessions, + ]); + } + async notifyRoomKeys(roomKeys) { if (this._roomEncryption) { let retryEventIds = this._roomEncryption.applyRoomKeys(roomKeys); diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index 21739a1e..9859a4f6 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -122,6 +122,23 @@ function processTimelineEvent(data, eventEntry, isInitialSync, isTimelineOpen, o data.lastMessageBody = body; } } + // store the event key of the last decrypted event so when decryption does succeed, + // we can attempt to re-decrypt from this point to update the room summary + if (!!data.encryption && eventEntry.isEncrypted && eventEntry.isDecrypted) { + let hasLargerEventKey = true; + if (data.lastDecryptedEventKey) { + try { + hasLargerEventKey = eventEntry.compare(data.lastDecryptedEventKey) > 0; + } catch (err) { + hasLargerEventKey = false; + } + } + if (hasLargerEventKey) { + data = data.cloneIfNeeded(); + const {fragmentId, entryIndex} = eventEntry; + data.lastDecryptedEventKey = {fragmentId, entryIndex}; + } + } return data; } @@ -155,6 +172,7 @@ class SummaryData { this.lastMessageTimestamp = copy ? copy.lastMessageTimestamp : null; this.isUnread = copy ? copy.isUnread : false; this.encryption = copy ? copy.encryption : null; + this.lastDecryptedEventKey = copy ? copy.lastDecryptedEventKey : null; this.isDirectMessage = copy ? copy.isDirectMessage : false; this.membership = copy ? copy.membership : null; this.inviteCount = copy ? copy.inviteCount : 0; @@ -261,6 +279,10 @@ export class RoomSummary { return this._data.tags; } + get lastDecryptedEventKey() { + return this._data.lastDecryptedEventKey; + } + writeClearUnread(txn) { const data = new SummaryData(this._data); data.isUnread = false; @@ -290,9 +312,6 @@ export class RoomSummary { processTimelineEntries(timelineEntries, isInitialSync, isTimelineOpen) { // clear cloned flag, so cloneIfNeeded makes a copy and // this._data is not modified if any field is changed. - - processTimelineEvent - this._data.cloned = false; const data = applyTimelineEntries( this._data, diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 81e31112..410c3d63 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -82,6 +82,10 @@ export class EventEntry extends BaseEntry { return this._eventEntry.event.type === "m.room.encrypted"; } + get isDecrypted() { + return !!this._decryptionResult?.event; + } + get isVerified() { return this.isEncrypted && this._decryptionResult?.isVerified; } diff --git a/src/matrix/room/timeline/persistence/TimelineReader.js b/src/matrix/room/timeline/persistence/TimelineReader.js index f5983a19..7e832b32 100644 --- a/src/matrix/room/timeline/persistence/TimelineReader.js +++ b/src/matrix/room/timeline/persistence/TimelineReader.js @@ -37,6 +37,44 @@ class ReaderRequest { } } + +export async function readFromWithTxn(eventKey, direction, amount, r, txn) { + let entries = []; + const timelineStore = txn.timelineEvents; + const fragmentStore = txn.timelineFragments; + + while (entries.length < amount && eventKey) { + let eventsWithinFragment; + if (direction.isForward) { + eventsWithinFragment = await timelineStore.eventsAfter(this._roomId, eventKey, amount); + } else { + eventsWithinFragment = await timelineStore.eventsBefore(this._roomId, eventKey, amount); + } + let eventEntries = eventsWithinFragment.map(e => new EventEntry(e, this._fragmentIdComparer)); + entries = directionalConcat(entries, eventEntries, direction); + // prepend or append eventsWithinFragment to entries, and wrap them in EventEntry + + if (entries.length < amount) { + const fragment = await fragmentStore.get(this._roomId, eventKey.fragmentId); + // this._fragmentIdComparer.addFragment(fragment); + let fragmentEntry = new FragmentBoundaryEntry(fragment, direction.isBackward, this._fragmentIdComparer); + // append or prepend fragmentEntry, reuse func from GapWriter? + directionalAppend(entries, fragmentEntry, direction); + // only continue loading if the fragment boundary can't be backfilled + if (!fragmentEntry.token && fragmentEntry.hasLinkedFragment) { + const nextFragment = await fragmentStore.get(this._roomId, fragmentEntry.linkedFragmentId); + this._fragmentIdComparer.add(nextFragment); + const nextFragmentEntry = new FragmentBoundaryEntry(nextFragment, direction.isForward, this._fragmentIdComparer); + directionalAppend(entries, nextFragmentEntry, direction); + eventKey = nextFragmentEntry.asEventKey(); + } else { + eventKey = null; + } + } + } + return entries; +} + export class TimelineReader { constructor({roomId, storage, fragmentIdComparer}) { this._roomId = roomId; @@ -87,40 +125,7 @@ export class TimelineReader { } async _readFrom(eventKey, direction, amount, r, txn) { - let entries = []; - const timelineStore = txn.timelineEvents; - const fragmentStore = txn.timelineFragments; - - while (entries.length < amount && eventKey) { - let eventsWithinFragment; - if (direction.isForward) { - eventsWithinFragment = await timelineStore.eventsAfter(this._roomId, eventKey, amount); - } else { - eventsWithinFragment = await timelineStore.eventsBefore(this._roomId, eventKey, amount); - } - let eventEntries = eventsWithinFragment.map(e => new EventEntry(e, this._fragmentIdComparer)); - entries = directionalConcat(entries, eventEntries, direction); - // prepend or append eventsWithinFragment to entries, and wrap them in EventEntry - - if (entries.length < amount) { - const fragment = await fragmentStore.get(this._roomId, eventKey.fragmentId); - // this._fragmentIdComparer.addFragment(fragment); - let fragmentEntry = new FragmentBoundaryEntry(fragment, direction.isBackward, this._fragmentIdComparer); - // append or prepend fragmentEntry, reuse func from GapWriter? - directionalAppend(entries, fragmentEntry, direction); - // only continue loading if the fragment boundary can't be backfilled - if (!fragmentEntry.token && fragmentEntry.hasLinkedFragment) { - const nextFragment = await fragmentStore.get(this._roomId, fragmentEntry.linkedFragmentId); - this._fragmentIdComparer.add(nextFragment); - const nextFragmentEntry = new FragmentBoundaryEntry(nextFragment, direction.isForward, this._fragmentIdComparer); - directionalAppend(entries, nextFragmentEntry, direction); - eventKey = nextFragmentEntry.asEventKey(); - } else { - eventKey = null; - } - } - } - + const entries = readFromWithTxn(eventKey, direction, amount, r, txn); if (this._decryptEntries) { r.decryptRequest = this._decryptEntries(entries, txn); try { From a8392dc684bf4e37e849d12bf8758486d3e3d6dc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 23 Sep 2020 14:26:14 +0200 Subject: [PATCH 02/12] also decrypt messages in the sync response that enabled encryption like initial sync --- src/matrix/Sync.js | 34 +++----- src/matrix/room/Room.js | 119 +++++++++++++------------ src/matrix/room/RoomSummary.js | 140 ++++++------------------------ src/matrix/room/members/Heroes.js | 8 +- src/matrix/room/members/load.js | 2 +- 5 files changed, 111 insertions(+), 192 deletions(-) diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index 515c096d..1223e1a1 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -41,17 +41,14 @@ function timelineIsEmpty(roomResponse) { /** * Sync steps in js-pseudocode: * ```js - * let preparation; - * if (room.needsPrepareSync) { - * // can only read some stores - * preparation = await room.prepareSync(roomResponse, prepareTxn); - * // can do async work that is not related to storage (such as decryption) - * preparation = await room.afterPrepareSync(preparation); - * } + * // can only read some stores + * const preparation = await room.prepareSync(roomResponse, membership, prepareTxn); + * // can do async work that is not related to storage (such as decryption) + * await room.afterPrepareSync(preparation); * // writes and calculates changes - * const changes = await room.writeSync(roomResponse, membership, isInitialSync, preparation, syncTxn); + * const changes = await room.writeSync(roomResponse, isInitialSync, preparation, syncTxn); * // applies and emits changes once syncTxn is committed - * room.afterSync(changes); + * room.afterSync(changes, preparation); * if (room.needsAfterSyncCompleted(changes)) { * // can do network requests * await room.afterSyncCompleted(changes); @@ -173,14 +170,14 @@ export class Sync { const isInitialSync = !syncToken; syncToken = response.next_batch; const roomStates = this._parseRoomsResponse(response.rooms, isInitialSync); - await this._prepareRooms(roomStates); + await this._prepareRooms(roomStates, isInitialSync); let sessionChanges; const syncTxn = await this._openSyncTxn(); try { await Promise.all(roomStates.map(async rs => { console.log(` * applying sync response to room ${rs.room.id} ...`); rs.changes = await rs.room.writeSync( - rs.roomResponse, rs.membership, isInitialSync, rs.preparation, syncTxn); + rs.roomResponse, isInitialSync, rs.preparation, syncTxn); })); sessionChanges = await this._session.writeSync(response, syncFilterId, syncTxn); } catch(err) { @@ -219,16 +216,11 @@ export class Sync { } async _prepareRooms(roomStates) { - const prepareRoomStates = roomStates.filter(rs => rs.room.needsPrepareSync); - if (prepareRoomStates.length) { - const prepareTxn = await this._openPrepareSyncTxn(); - await Promise.all(prepareRoomStates.map(async rs => { - rs.preparation = await rs.room.prepareSync(rs.roomResponse, prepareTxn); - })); - await Promise.all(prepareRoomStates.map(async rs => { - rs.preparation = await rs.room.afterPrepareSync(rs.preparation); - })); - } + const prepareTxn = await this._openPrepareSyncTxn(); + await Promise.all(roomStates.map(async rs => { + rs.preparation = await rs.room.prepareSync(rs.roomResponse, rs.membership, prepareTxn); + })); + await Promise.all(roomStates.map(rs => rs.room.afterPrepareSync(rs.preparation))); } async _openSyncTxn() { diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 2b9c3355..2688c6b9 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -37,7 +37,7 @@ export class Room extends EventEmitter { this._storage = storage; this._hsApi = hsApi; this._mediaRepository = mediaRepository; - this._summary = new RoomSummary(roomId, user.id); + this._summary = new RoomSummary(roomId); this._fragmentIdComparer = new FragmentIdComparer([]); this._syncWriter = new SyncWriter({roomId, fragmentIdComparer: this._fragmentIdComparer}); this._emitCollectionChange = emitCollectionChange; @@ -84,18 +84,17 @@ export class Room extends EventEmitter { // _decryptEntries entries and could even know which events have been decrypted for the first // time from DecryptionChanges.write and only pass those to the summary. As timeline changes // are not essential to the room summary, it's fine to write this in a separate txn for now. - const changes = this._summary.processTimelineEntries(retryEntries, false, this._isTimelineOpen); - if (changes) { - this._summary.writeAndApplyChanges(changes, this._storage); + const changes = this._summary.data.applyTimelineEntries(retryEntries, false, this._isTimelineOpen); + if (await this._summary.writeAndApplyData(changes, this._storage)) { this._emitUpdate(); } } } } - _enableEncryption(encryptionParams) { - this._roomEncryption = this._createRoomEncryption(this, encryptionParams); - if (this._roomEncryption) { + _setEncryption(roomEncryption) { + if (roomEncryption && !this._roomEncryption) { + this._roomEncryption = roomEncryption; this._sendQueue.enableEncryption(this._roomEncryption); if (this._timeline) { this._timeline.enableEncryption(this._decryptEntries.bind(this, DecryptionSource.Timeline)); @@ -141,57 +140,62 @@ export class Room extends EventEmitter { return request; } - get needsPrepareSync() { - // only encrypted rooms need the prepare sync steps - return !!this._roomEncryption; - } + async prepareSync(roomResponse, membership, txn) { + const summaryChanges = this._summary.data.applySyncResponse(roomResponse, membership) + let roomEncryption = this._roomEncryption; + // encryption is enabled in this sync + if (!roomEncryption && summaryChanges.encryption) { + roomEncryption = this._createRoomEncryption(this, summaryChanges.encryption); + } - async prepareSync(roomResponse, txn) { - if (this._roomEncryption) { + let decryptPreparation; + if (roomEncryption) { const events = roomResponse?.timeline?.events; if (Array.isArray(events)) { const eventsToDecrypt = events.filter(event => { return event?.type === EVENT_ENCRYPTED_TYPE; }); - const preparation = await this._roomEncryption.prepareDecryptAll( + decryptPreparation = await roomEncryption.prepareDecryptAll( eventsToDecrypt, DecryptionSource.Sync, this._isTimelineOpen, txn); - return preparation; } } + + return { + roomEncryption, + summaryChanges, + decryptPreparation, + decryptChanges: null, + }; } async afterPrepareSync(preparation) { - if (preparation) { - const decryptChanges = await preparation.decrypt(); - return decryptChanges; + if (preparation.decryptPreparation) { + preparation.decryptChanges = await preparation.decryptPreparation.decrypt(); + preparation.decryptPreparation = null; } } /** @package */ - async writeSync(roomResponse, membership, isInitialSync, decryptChanges, txn) { - let decryption; - if (this._roomEncryption && decryptChanges) { - decryption = await decryptChanges.write(txn); - } + async writeSync(roomResponse, isInitialSync, {summaryChanges, decryptChanges, roomEncryption}, txn) { const {entries, newLiveKey, memberChanges} = await this._syncWriter.writeSync(roomResponse, txn); - if (decryption) { + if (decryptChanges) { + const decryption = await decryptChanges.write(txn); decryption.applyToEntries(entries); } // pass member changes to device tracker - if (this._roomEncryption && this.isTrackingMembers && memberChanges?.size) { - await this._roomEncryption.writeMemberChanges(memberChanges, txn); + if (roomEncryption && this.isTrackingMembers && memberChanges?.size) { + await roomEncryption.writeMemberChanges(memberChanges, txn); } - const summaryChanges = this._summary.writeSync( - roomResponse, - entries, - membership, - isInitialSync, this._isTimelineOpen, - txn); + // also apply (decrypted) timeline entries to the summary changes + summaryChanges = summaryChanges.applyTimelineEntries( + entries, isInitialSync, this._isTimelineOpen, this._user.id); + // write summary changes, and unset if nothing was actually changed + summaryChanges = this._summary.writeData(summaryChanges, txn); // fetch new members while we have txn open, // but don't make any in-memory changes yet let heroChanges; - if (summaryChanges && needsHeroes(summaryChanges)) { + if (summaryChanges?.needsHeroes) { // room name disappeared, open heroes if (!this._heroes) { this._heroes = new Heroes(this._roomId); @@ -204,6 +208,7 @@ export class Room extends EventEmitter { } return { summaryChanges, + roomEncryption, newTimelineEntries: entries, newLiveKey, removedPendingEvents, @@ -217,11 +222,9 @@ export class Room extends EventEmitter { * Called with the changes returned from `writeSync` to apply them and emit changes. * No storage or network operations should be done here. */ - afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, memberChanges, heroChanges}) { + afterSync({summaryChanges, newTimelineEntries, newLiveKey, removedPendingEvents, memberChanges, heroChanges, roomEncryption}) { this._syncWriter.afterSync(newLiveKey); - if (!this._summary.encryption && summaryChanges.encryption && !this._roomEncryption) { - this._enableEncryption(summaryChanges.encryption); - } + this._setEncryption(roomEncryption); if (memberChanges.size) { if (this._changedMembersDuringSync) { for (const [userId, memberChange] of memberChanges.entries()) { @@ -235,14 +238,14 @@ export class Room extends EventEmitter { let emitChange = false; if (summaryChanges) { this._summary.applyChanges(summaryChanges); - if (!this._summary.needsHeroes) { + if (!this._summary.data.needsHeroes) { this._heroes = null; } emitChange = true; } if (this._heroes && heroChanges) { const oldName = this.name; - this._heroes.applyChanges(heroChanges, this._summary); + this._heroes.applyChanges(heroChanges, this._summary.data); if (oldName !== this.name) { emitChange = true; } @@ -294,14 +297,15 @@ export class Room extends EventEmitter { async load(summary, txn) { try { this._summary.load(summary); - if (this._summary.encryption) { - this._enableEncryption(this._summary.encryption); + if (this._summary.data.encryption) { + const roomEncryption = this._createRoomEncryption(this, this._summary.data.encryption); + this._setEncryption(roomEncryption); } // need to load members for name? - if (this._summary.needsHeroes) { + if (this._summary.data.needsHeroes) { this._heroes = new Heroes(this._roomId); - const changes = await this._heroes.calculateChanges(this._summary.heroes, [], txn); - this._heroes.applyChanges(changes, this._summary); + const changes = await this._heroes.calculateChanges(this._summary.data.heroes, [], txn); + this._heroes.applyChanges(changes, this._summary.data); } return this._syncWriter.load(txn); } catch (err) { @@ -397,7 +401,14 @@ export class Room extends EventEmitter { if (this._heroes) { return this._heroes.roomName; } - return this._summary.name; + const summaryData = this._summary.data; + if (summaryData.name) { + return summaryData.name; + } + if (summaryData.canonicalAlias) { + return summaryData.canonicalAlias; + } + return null; } /** @public */ @@ -406,8 +417,8 @@ export class Room extends EventEmitter { } get avatarUrl() { - if (this._summary.avatarUrl) { - return this._summary.avatarUrl; + if (this._summary.data.avatarUrl) { + return this._summary.data.avatarUrl; } else if (this._heroes) { return this._heroes.roomAvatarUrl; } @@ -415,28 +426,28 @@ export class Room extends EventEmitter { } get lastMessageTimestamp() { - return this._summary.lastMessageTimestamp; + return this._summary.data.lastMessageTimestamp; } get isUnread() { - return this._summary.isUnread; + return this._summary.data.isUnread; } get notificationCount() { - return this._summary.notificationCount; + return this._summary.data.notificationCount; } get highlightCount() { - return this._summary.highlightCount; + return this._summary.data.highlightCount; } get isLowPriority() { - const tags = this._summary.tags; + const tags = this._summary.data.tags; return !!(tags && tags['m.lowpriority']); } get isEncrypted() { - return !!this._summary.encryption; + return !!this._summary.data.encryption; } enableSessionBackup(sessionBackup) { @@ -444,7 +455,7 @@ export class Room extends EventEmitter { } get isTrackingMembers() { - return this._summary.isTrackingMembers; + return this._summary.data.isTrackingMembers; } async _getLastEventId() { diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index 9859a4f6..1ec55d5c 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -39,16 +39,17 @@ function applySyncResponse(data, roomResponse, membership) { if (roomResponse.account_data) { data = roomResponse.account_data.events.reduce(processRoomAccountData, data); } + const stateEvents = roomResponse?.state?.events; // state comes before timeline - if (roomResponse.state) { - data = roomResponse.state.events.reduce(processStateEvent, data); + if (Array.isArray(stateEvents)) { + data = stateEvents.reduce(processStateEvent, data); } - const {timeline} = roomResponse; + const timelineEvents = roomResponse?.timeline?.events; // process state events in timeline // non-state events are handled by applyTimelineEntries // so decryption is handled properly - if (timeline && Array.isArray(timeline.events)) { - data = timeline.events.reduce((data, event) => { + if (Array.isArray(timelineEvents)) { + data = timelineEvents.reduce((data, event) => { if (typeof event.state_key === "string") { return processStateEvent(data, event); } @@ -200,87 +201,27 @@ class SummaryData { const {cloned, ...serializedProps} = this; return serializedProps; } -} -export function needsHeroes(data) { - return !data.name && !data.canonicalAlias && data.heroes && data.heroes.length > 0; + applyTimelineEntries(timelineEntries, isInitialSync, isTimelineOpen, ownUserId) { + return applyTimelineEntries(this, timelineEntries, isInitialSync, isTimelineOpen, ownUserId); + } + + applySyncResponse(roomResponse, membership) { + return applySyncResponse(this, roomResponse, membership); + } + + get needsHeroes() { + return !this.name && !this.canonicalAlias && this.heroes && this.heroes.length > 0; + } } export class RoomSummary { - constructor(roomId, ownUserId) { - this._ownUserId = ownUserId; + constructor(roomId) { this._data = new SummaryData(null, roomId); } - get name() { - if (this._data.name) { - return this._data.name; - } - if (this._data.canonicalAlias) { - return this._data.canonicalAlias; - } - return null; - } - - get heroes() { - return this._data.heroes; - } - - get encryption() { - return this._data.encryption; - } - - // whether the room name should be determined with Heroes - get needsHeroes() { - return needsHeroes(this._data); - } - - get isUnread() { - return this._data.isUnread; - } - - get notificationCount() { - return this._data.notificationCount; - } - - get highlightCount() { - return this._data.highlightCount; - } - - get lastMessage() { - return this._data.lastMessageBody; - } - - get lastMessageTimestamp() { - return this._data.lastMessageTimestamp; - } - - get inviteCount() { - return this._data.inviteCount; - } - - get joinCount() { - return this._data.joinCount; - } - - get avatarUrl() { - return this._data.avatarUrl; - } - - get hasFetchedMembers() { - return this._data.hasFetchedMembers; - } - - get isTrackingMembers() { - return this._data.isTrackingMembers; - } - - get tags() { - return this._data.tags; - } - - get lastDecryptedEventKey() { - return this._data.lastDecryptedEventKey; + get data() { + return this._data; } writeClearUnread(txn) { @@ -306,45 +247,17 @@ export class RoomSummary { return data; } - /** - * after retrying decryption - */ - processTimelineEntries(timelineEntries, isInitialSync, isTimelineOpen) { - // clear cloned flag, so cloneIfNeeded makes a copy and - // this._data is not modified if any field is changed. - this._data.cloned = false; - const data = applyTimelineEntries( - this._data, - timelineEntries, - isInitialSync, isTimelineOpen, - this._ownUserId); - if (data !== this._data) { - return data; - } - } - - writeSync(roomResponse, timelineEntries, membership, isInitialSync, isTimelineOpen, txn) { - // clear cloned flag, so cloneIfNeeded makes a copy and - // this._data is not modified if any field is changed. - this._data.cloned = false; - let data = applySyncResponse(this._data, roomResponse, membership); - data = applyTimelineEntries( - data, - timelineEntries, - isInitialSync, isTimelineOpen, - this._ownUserId); + writeData(data, txn) { if (data !== this._data) { txn.roomSummary.set(data.serialize()); return data; } } - /** - * Only to be used with processTimelineEntries, - * other methods like writeSync, writeHasFetchedMembers, - * writeIsTrackingMembers, ... take a txn directly. - */ - async writeAndApplyChanges(data, storage) { + async writeAndApplyData(data, storage) { + if (data === this._data) { + return; + } const txn = await storage.readWriteTxn([ storage.storeNames.roomSummary, ]); @@ -360,6 +273,9 @@ export class RoomSummary { applyChanges(data) { this._data = data; + // clear cloned flag, so cloneIfNeeded makes a copy and + // this._data is not modified if any field is changed. + this._data.cloned = false; } async load(summary) { diff --git a/src/matrix/room/members/Heroes.js b/src/matrix/room/members/Heroes.js index 809b61c2..f6ad3085 100644 --- a/src/matrix/room/members/Heroes.js +++ b/src/matrix/room/members/Heroes.js @@ -16,8 +16,8 @@ limitations under the License. import {RoomMember} from "./RoomMember.js"; -function calculateRoomName(sortedMembers, summary) { - const countWithoutMe = summary.joinCount + summary.inviteCount - 1; +function calculateRoomName(sortedMembers, summaryData) { + const countWithoutMe = summaryData.joinCount + summaryData.inviteCount - 1; if (sortedMembers.length >= countWithoutMe) { if (sortedMembers.length > 1) { const lastMember = sortedMembers[sortedMembers.length - 1]; @@ -74,7 +74,7 @@ export class Heroes { return {updatedHeroMembers: updatedHeroMembers.values(), removedUserIds}; } - applyChanges({updatedHeroMembers, removedUserIds}, summary) { + applyChanges({updatedHeroMembers, removedUserIds}, summaryData) { for (const userId of removedUserIds) { this._members.delete(userId); } @@ -82,7 +82,7 @@ export class Heroes { this._members.set(member.userId, member); } const sortedMembers = Array.from(this._members.values()).sort((a, b) => a.name.localeCompare(b.name)); - this._roomName = calculateRoomName(sortedMembers, summary); + this._roomName = calculateRoomName(sortedMembers, summaryData); } get roomName() { diff --git a/src/matrix/room/members/load.js b/src/matrix/room/members/load.js index 18fc4eb4..a8648eac 100644 --- a/src/matrix/room/members/load.js +++ b/src/matrix/room/members/load.js @@ -82,7 +82,7 @@ async function fetchMembers({summary, syncToken, roomId, hsApi, storage, setChan export async function fetchOrLoadMembers(options) { const {summary} = options; - if (!summary.hasFetchedMembers) { + if (!summary.data.hasFetchedMembers) { return fetchMembers(options); } else { return loadMembers(options); From 9d41e122a0461f1d9faf0a1b42e613e2df34ea5e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 23 Sep 2020 17:34:25 +0200 Subject: [PATCH 03/12] draft of falling back to reading entries since last decrypted event key this change notifyRoomKey(s) to only take one room key at a time to simplify things --- src/matrix/DeviceMessageHandler.js | 14 +- src/matrix/e2ee/RoomEncryption.js | 131 +++++++++++++----- src/matrix/room/Room.js | 87 +++++++----- src/matrix/room/timeline/EventKey.js | 4 + .../timeline/persistence/TimelineReader.js | 5 + 5 files changed, 165 insertions(+), 76 deletions(-) diff --git a/src/matrix/DeviceMessageHandler.js b/src/matrix/DeviceMessageHandler.js index 4c7d0e75..8a50c66c 100644 --- a/src/matrix/DeviceMessageHandler.js +++ b/src/matrix/DeviceMessageHandler.js @@ -59,12 +59,12 @@ export class DeviceMessageHandler { return {roomKeys}; } - _applyDecryptChanges(rooms, {roomKeys}) { - if (roomKeys && roomKeys.length) { - const roomKeysByRoom = groupBy(roomKeys, s => s.roomId); - for (const [roomId, roomKeys] of roomKeysByRoom) { - const room = rooms.get(roomId); - room?.notifyRoomKeys(roomKeys); + async _applyDecryptChanges(rooms, {roomKeys}) { + if (Array.isArray(roomKeys)) { + for (const roomKey of roomKeys) { + const room = rooms.get(roomKey.roomId); + // TODO: this is less parallized than it could be (like sync) + await room?.notifyRoomKey(roomKey); } } } @@ -101,7 +101,7 @@ export class DeviceMessageHandler { throw err; } await txn.complete(); - this._applyDecryptChanges(rooms, changes); + await this._applyDecryptChanges(rooms, changes); } async _getPendingEvents(txn) { diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index f0446f98..7b45a60c 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -42,8 +42,12 @@ export class RoomEncryption { this._megolmBackfillCache = this._megolmDecryption.createSessionCache(); this._megolmSyncCache = this._megolmDecryption.createSessionCache(); - // not `event_id`, but an internal event id passed in to the decrypt methods - this._eventIdsByMissingSession = new Map(); + // session => event ids of messages we tried to decrypt and the session was missing + this._missingSessions = new SessionToEventIdsMap(); + // sessions that may or may not be missing, but that while + // looking for a particular session came up as a candidate and were + // added to the cache to prevent further lookups from storage + this._missingSessionCandidates = new SessionToEventIdsMap(); this._senderDeviceCache = new Map(); this._storage = storage; this._sessionBackup = sessionBackup; @@ -57,8 +61,7 @@ export class RoomEncryption { return; } this._sessionBackup = sessionBackup; - for(const key of this._eventIdsByMissingSession.keys()) { - const {senderKey, sessionId} = decodeMissingSessionKey(key); + for(const {senderKey, sessionId} of this._missingSessions.getSessions()) { await this._requestMissingSessionFromBackup(senderKey, sessionId, null); } } @@ -115,13 +118,17 @@ export class RoomEncryption { if (customCache) { customCache.dispose(); } - return new DecryptionPreparation(preparation, errors, {isTimelineOpen, source}, this); + return new DecryptionPreparation(preparation, errors, {isTimelineOpen, source}, this, events); } - async _processDecryptionResults(results, errors, flags, txn) { - for (const error of errors.values()) { - if (error.code === "MEGOLM_NO_SESSION") { - this._addMissingSessionEvent(error.event, flags.source); + async _processDecryptionResults(events, results, errors, flags, txn) { + for (const event of events) { + const error = errors.get(event.event_id); + if (error?.code === "MEGOLM_NO_SESSION") { + this._addMissingSessionEvent(event, flags.source); + } else { + this._missingSessions.removeEvent(event); + this._missingSessionCandidates.removeEvent(event); } } if (flags.isTimelineOpen) { @@ -145,17 +152,12 @@ export class RoomEncryption { } _addMissingSessionEvent(event, source) { - const senderKey = event.content?.["sender_key"]; - const sessionId = event.content?.["session_id"]; - const key = encodeMissingSessionKey(senderKey, sessionId); - let eventIds = this._eventIdsByMissingSession.get(key); - // new missing session - if (!eventIds) { + const isNewSession = this._missingSessions.addEvent(event); + if (isNewSession) { + const senderKey = event.content?.["sender_key"]; + const sessionId = event.content?.["session_id"]; this._requestMissingSessionFromBackup(senderKey, sessionId, source); - eventIds = new Set(); - this._eventIdsByMissingSession.set(key, eventIds); } - eventIds.add(event.event_id); } async _requestMissingSessionFromBackup(senderKey, sessionId, source) { @@ -163,7 +165,7 @@ export class RoomEncryption { // and only after that proceed to request from backup if (source === DecryptionSource.Sync) { await this._clock.createTimeout(10000).elapsed(); - if (this._disposed || !this._eventIdsByMissingSession.has(encodeMissingSessionKey(senderKey, sessionId))) { + if (this._disposed || !this._missingSessions.hasSession(senderKey, sessionId)) { return; } } @@ -192,8 +194,8 @@ export class RoomEncryption { await txn.complete(); if (roomKey) { - // this will call into applyRoomKeys below - await this._room.notifyRoomKeys([roomKey]); + // this will reattempt decryption + await this._room.notifyRoomKey(roomKey); } } else if (session?.algorithm) { console.info(`Backed-up session of unknown algorithm: ${session.algorithm}`); @@ -212,18 +214,17 @@ export class RoomEncryption { * @param {Array} roomKeys * @return {Array} the event ids that should be retried to decrypt */ - applyRoomKeys(roomKeys) { - // retry decryption with the new sessions - const retryEventIds = []; - for (const roomKey of roomKeys) { - const key = encodeMissingSessionKey(roomKey.senderKey, roomKey.sessionId); - const entriesForSession = this._eventIdsByMissingSession.get(key); - if (entriesForSession) { - this._eventIdsByMissingSession.delete(key); - retryEventIds.push(...entriesForSession); - } + getEventIdsForRoomKey(roomKey) { + let eventIds = this._missingSessions.getEventIds(roomKey.senderKey, roomKey.sessionId); + if (!eventIds) { + eventIds = this._missingSessionCandidates.getEventIds(roomKey.senderKey, roomKey.sessionId); } - return retryEventIds; + return eventIds; + } + + findAndCacheEntriesForRoomKey(roomKey, candidateEntries) { + // add all to _missingSessionCandidates + // filter messages to roomKey } async encrypt(type, content, hsApi) { @@ -354,11 +355,12 @@ export class RoomEncryption { * the decryption results before turning them */ class DecryptionPreparation { - constructor(megolmDecryptionPreparation, extraErrors, flags, roomEncryption) { + constructor(megolmDecryptionPreparation, extraErrors, flags, roomEncryption, events) { this._megolmDecryptionPreparation = megolmDecryptionPreparation; this._extraErrors = extraErrors; this._flags = flags; this._roomEncryption = roomEncryption; + this._events = events; } async decrypt() { @@ -366,7 +368,8 @@ class DecryptionPreparation { await this._megolmDecryptionPreparation.decrypt(), this._extraErrors, this._flags, - this._roomEncryption); + this._roomEncryption, + this._events); } dispose() { @@ -375,17 +378,18 @@ class DecryptionPreparation { } class DecryptionChanges { - constructor(megolmDecryptionChanges, extraErrors, flags, roomEncryption) { + constructor(megolmDecryptionChanges, extraErrors, flags, roomEncryption, events) { this._megolmDecryptionChanges = megolmDecryptionChanges; this._extraErrors = extraErrors; this._flags = flags; this._roomEncryption = roomEncryption; + this._events = events; } async write(txn) { const {results, errors} = await this._megolmDecryptionChanges.write(txn); mergeMap(this._extraErrors, errors); - await this._roomEncryption._processDecryptionResults(results, errors, this._flags, txn); + await this._roomEncryption._processDecryptionResults(this._events, results, errors, this._flags, txn); return new BatchDecryptionResult(results, errors); } } @@ -410,3 +414,58 @@ class BatchDecryptionResult { } } } + +class SessionToEventIdsMap { + constructor() { + this._eventIdsBySession = new Map(); + } + + addEvent(event) { + let isNewSession = false; + const senderKey = event.content?.["sender_key"]; + const sessionId = event.content?.["session_id"]; + const key = encodeMissingSessionKey(senderKey, sessionId); + let eventIds = this._eventIdsBySession.get(key); + // new missing session + if (!eventIds) { + eventIds = new Set(); + this._eventIdsBySession.set(key, eventIds); + isNewSession = true; + } + eventIds.add(event.event_id); + return isNewSession; + } + + getEventIds(senderKey, sessionId) { + const key = encodeMissingSessionKey(senderKey, sessionId); + const entriesForSession = this._eventIdsBySession.get(key); + if (entriesForSession) { + return [...entriesForSession]; + } + } + + getSessions() { + return Array.from(this._eventIdsBySession.keys()).map(decodeMissingSessionKey); + } + + hasSession(senderKey, sessionId) { + return this._eventIdsBySession.has(encodeMissingSessionKey(senderKey, sessionId)); + } + + removeEvent(event) { + let hasRemovedSession = false; + const senderKey = event.content?.["sender_key"]; + const sessionId = event.content?.["session_id"]; + const key = encodeMissingSessionKey(senderKey, sessionId); + let eventIds = this._eventIdsBySession.get(key); + if (eventIds) { + if (eventIds.delete(event.event_id)) { + if (!eventIds.length) { + this._eventIdsBySession.delete(key); + hasRemovedSession = true; + } + } + } + return hasRemovedSession; + } +} diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 2688c6b9..c9a490cb 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -52,42 +52,63 @@ export class Room extends EventEmitter { this._clock = clock; } - _readEntriesToRetryDecryption(retryEventIds) { - const readFromEventKey = retryEventIds.length !== 0; - const stores = - const txn = await this._storage.readTxn([ - this._storage.storeNames.timelineEvents, - this._storage.storeNames.inboundGroupSessions, - ]); + _readRetryDecryptCandidateEntries(sinceEventKey, txn) { + if (sinceEventKey) { + return readFromWithTxn(sinceEventKey, Direction.Forward, Number.MAX_SAFE_INTEGER, txn); + } else { + // all messages for room + return readFromWithTxn(this._syncWriter.lastMessageKey, Direction.Backward, Number.MAX_SAFE_INTEGER, txn); + } } - async notifyRoomKeys(roomKeys) { - if (this._roomEncryption) { - let retryEventIds = this._roomEncryption.applyRoomKeys(roomKeys); - if (retryEventIds.length) { - const retryEntries = []; - const txn = await this._storage.readTxn([ - this._storage.storeNames.timelineEvents, - this._storage.storeNames.inboundGroupSessions, - ]); - for (const eventId of retryEventIds) { - const storageEntry = await txn.timelineEvents.getByEventId(this._roomId, eventId); - if (storageEntry) { - retryEntries.push(new EventEntry(storageEntry, this._fragmentIdComparer)); - } + async notifyRoomKey(roomKey) { + if (!this._roomEncryption) { + return; + } + const retryEventIds = this._roomEncryption.getEventIdsForRoomKey(roomKey); + const stores = [ + this._storage.storeNames.timelineEvents, + this._storage.storeNames.inboundGroupSessions, + ]; + let txn; + let retryEntries; + if (retryEventIds) { + retryEntries = []; + txn = await this._storage.readTxn(stores); + for (const eventId of retryEventIds) { + const storageEntry = await txn.timelineEvents.getByEventId(this._roomId, eventId); + if (storageEntry) { + retryEntries.push(new EventEntry(storageEntry, this._fragmentIdComparer)); } - const decryptRequest = this._decryptEntries(DecryptionSource.Retry, retryEntries, txn); - await decryptRequest.complete(); + } + } else { + // we only look for messages since lastDecryptedEventKey because + // the timeline must be closed (otherwise getEventIdsForRoomKey would have found the event ids) + // and to update the summary we only care about events since lastDecryptedEventKey + const key = this._summary.data.lastDecryptedEventKey; + // key might be missing if we haven't decrypted any events in this room + const sinceEventKey = key && new EventKey(key.fragmentId, key.entryIndex); + // check we have not already decrypted the most recent event in the room + // otherwise we know that the messages for this room key will not update the room summary + if (!sinceEventKey || !sinceEventKey.equals(this._syncWriter.lastMessageKey)) { + txn = await this._storage.readTxn(stores.concat(this._storage.storeNames.timelineFragments)); + const candidateEntries = await this._readRetryDecryptCandidateEntries(sinceEventKey, txn); + retryEntries = this._roomEncryption.findAndCacheEntriesForRoomKey(roomKey, candidateEntries); + } + } + if (retryEntries?.length) { + const decryptRequest = this._decryptEntries(DecryptionSource.Retry, retryEntries, txn); + // this will close txn while awaiting decryption + await decryptRequest.complete(); - this._timeline?.replaceEntries(retryEntries); - // we would ideally write the room summary in the same txn as the groupSessionDecryptions in the - // _decryptEntries entries and could even know which events have been decrypted for the first - // time from DecryptionChanges.write and only pass those to the summary. As timeline changes - // are not essential to the room summary, it's fine to write this in a separate txn for now. - const changes = this._summary.data.applyTimelineEntries(retryEntries, false, this._isTimelineOpen); - if (await this._summary.writeAndApplyData(changes, this._storage)) { - this._emitUpdate(); - } + this._timeline?.replaceEntries(retryEntries); + // we would ideally write the room summary in the same txn as the groupSessionDecryptions in the + // _decryptEntries entries and could even know which events have been decrypted for the first + // time from DecryptionChanges.write and only pass those to the summary. As timeline changes + // are not essential to the room summary, it's fine to write this in a separate txn for now. + const changes = this._summary.data.applyTimelineEntries(retryEntries, false, this._isTimelineOpen); + if (await this._summary.writeAndApplyData(changes, this._storage)) { + this._emitUpdate(); } } } @@ -203,7 +224,7 @@ export class Room extends EventEmitter { heroChanges = await this._heroes.calculateChanges(summaryChanges.heroes, memberChanges, txn); } let removedPendingEvents; - if (roomResponse.timeline && roomResponse.timeline.events) { + if (Array.isArray(roomResponse.timeline?.events)) { removedPendingEvents = this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn); } return { diff --git a/src/matrix/room/timeline/EventKey.js b/src/matrix/room/timeline/EventKey.js index 128f8805..e1771258 100644 --- a/src/matrix/room/timeline/EventKey.js +++ b/src/matrix/room/timeline/EventKey.js @@ -63,6 +63,10 @@ export class EventKey { toString() { return `[${this.fragmentId}/${this.eventIndex}]`; } + + equals(other) { + return this.fragmentId === other?.fragmentId && this.eventIndex === other?.eventIndex; + } } export function xtests() { diff --git a/src/matrix/room/timeline/persistence/TimelineReader.js b/src/matrix/room/timeline/persistence/TimelineReader.js index 7e832b32..b2bb8fbf 100644 --- a/src/matrix/room/timeline/persistence/TimelineReader.js +++ b/src/matrix/room/timeline/persistence/TimelineReader.js @@ -46,6 +46,7 @@ export async function readFromWithTxn(eventKey, direction, amount, r, txn) { while (entries.length < amount && eventKey) { let eventsWithinFragment; if (direction.isForward) { + // TODO: should we pass amount - entries.length here? eventsWithinFragment = await timelineStore.eventsAfter(this._roomId, eventKey, amount); } else { eventsWithinFragment = await timelineStore.eventsBefore(this._roomId, eventKey, amount); @@ -56,6 +57,10 @@ export async function readFromWithTxn(eventKey, direction, amount, r, txn) { if (entries.length < amount) { const fragment = await fragmentStore.get(this._roomId, eventKey.fragmentId); + // TODO: why does the first fragment not need to be added? (the next *is* added below) + // it looks like this would be fine when loading in the sync island + // (as the live fragment should be added already) but not for permalinks when we support them + // // this._fragmentIdComparer.addFragment(fragment); let fragmentEntry = new FragmentBoundaryEntry(fragment, direction.isBackward, this._fragmentIdComparer); // append or prepend fragmentEntry, reuse func from GapWriter? From d53b5eefb393a709bc202f42d56136c430cf69d5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 23 Sep 2020 17:59:42 +0200 Subject: [PATCH 04/12] fill in the blanks --- src/matrix/e2ee/RoomEncryption.js | 21 +++++++++++++++++-- src/matrix/room/Room.js | 14 +++++++++---- src/matrix/room/RoomSummary.js | 6 ++++++ .../timeline/persistence/TimelineReader.js | 9 +++++--- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 7b45a60c..dac362c0 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -222,9 +222,26 @@ export class RoomEncryption { return eventIds; } + /** + * caches mapping of session to event id of all encrypted candidates + * and filters to return only the candidates for the given room key + */ findAndCacheEntriesForRoomKey(roomKey, candidateEntries) { - // add all to _missingSessionCandidates - // filter messages to roomKey + const matches = []; + + for (const entry of candidateEntries) { + if (entry.eventType === ENCRYPTED_TYPE) { + this._missingSessionCandidates.addEvent(entry.event); + const senderKey = entry.event?.content?.["sender_key"]; + const sessionId = entry.event?.content?.["session_id"]; + if (senderKey === roomKey.senderKey && sessionId === roomKey.sessionId) { + matches.push(entry); + } + } + } + + return matches; + } async encrypt(type, content, hsApi) { diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index c9a490cb..a0cea456 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -15,9 +15,10 @@ limitations under the License. */ import {EventEmitter} from "../../utils/EventEmitter.js"; -import {RoomSummary, needsHeroes} from "./RoomSummary.js"; +import {RoomSummary} from "./RoomSummary.js"; import {SyncWriter} from "./timeline/persistence/SyncWriter.js"; import {GapWriter} from "./timeline/persistence/GapWriter.js"; +import {readRawTimelineEntriesWithTxn} from "./timeline/persistence/TimelineReader.js"; import {Timeline} from "./timeline/Timeline.js"; import {FragmentIdComparer} from "./timeline/FragmentIdComparer.js"; import {SendQueue} from "./sending/SendQueue.js"; @@ -26,6 +27,8 @@ import {fetchOrLoadMembers} from "./members/load.js"; import {MemberList} from "./members/MemberList.js"; import {Heroes} from "./members/Heroes.js"; import {EventEntry} from "./timeline/entries/EventEntry.js"; +import {EventKey} from "./timeline/EventKey.js"; +import {Direction} from "./timeline/Direction.js"; import {DecryptionSource} from "../e2ee/common.js"; const EVENT_ENCRYPTED_TYPE = "m.room.encrypted"; @@ -54,10 +57,13 @@ export class Room extends EventEmitter { _readRetryDecryptCandidateEntries(sinceEventKey, txn) { if (sinceEventKey) { - return readFromWithTxn(sinceEventKey, Direction.Forward, Number.MAX_SAFE_INTEGER, txn); + return readRawTimelineEntriesWithTxn(sinceEventKey, Direction.Forward, Number.MAX_SAFE_INTEGER, txn); } else { - // all messages for room - return readFromWithTxn(this._syncWriter.lastMessageKey, Direction.Backward, Number.MAX_SAFE_INTEGER, txn); + // all messages for room ... + // if you haven't decrypted any message in a room yet, + // it's unlikely you will have tons of them. + // so this should be fine as a last resort + return readRawTimelineEntriesWithTxn(this._syncWriter.lastMessageKey, Direction.Backward, Number.MAX_SAFE_INTEGER, txn); } } diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index 1ec55d5c..392bf755 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -131,6 +131,12 @@ function processTimelineEvent(data, eventEntry, isInitialSync, isTimelineOpen, o try { hasLargerEventKey = eventEntry.compare(data.lastDecryptedEventKey) > 0; } catch (err) { + // TODO: load the fragments in between here? + // this could happen if an earlier event gets decrypted that + // is in a fragment different from the live one and the timeline is not open. + // In this case, we will just read too many events once per app load + // and then keep the mapping in memory. When eventually an event is decrypted in + // the live fragment, this should stop failing and the event key will be written. hasLargerEventKey = false; } } diff --git a/src/matrix/room/timeline/persistence/TimelineReader.js b/src/matrix/room/timeline/persistence/TimelineReader.js index b2bb8fbf..bf4d2766 100644 --- a/src/matrix/room/timeline/persistence/TimelineReader.js +++ b/src/matrix/room/timeline/persistence/TimelineReader.js @@ -37,8 +37,11 @@ class ReaderRequest { } } - -export async function readFromWithTxn(eventKey, direction, amount, r, txn) { +/** + * Raw because it doesn't do decryption and in the future it should not read relations either. + * It is just about reading entries and following fragment links + */ +export async function readRawTimelineEntriesWithTxn(eventKey, direction, amount, r, txn) { let entries = []; const timelineStore = txn.timelineEvents; const fragmentStore = txn.timelineFragments; @@ -130,7 +133,7 @@ export class TimelineReader { } async _readFrom(eventKey, direction, amount, r, txn) { - const entries = readFromWithTxn(eventKey, direction, amount, r, txn); + const entries = readRawTimelineEntriesWithTxn(eventKey, direction, amount, r, txn); if (this._decryptEntries) { r.decryptRequest = this._decryptEntries(entries, txn); try { From 6e77ebb160a209bf0e5f22825b582e4eaeff4205 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 23 Sep 2020 18:06:16 +0200 Subject: [PATCH 05/12] undo obsolete changes --- src/matrix/Sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index 1223e1a1..f78e437c 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -48,7 +48,7 @@ function timelineIsEmpty(roomResponse) { * // writes and calculates changes * const changes = await room.writeSync(roomResponse, isInitialSync, preparation, syncTxn); * // applies and emits changes once syncTxn is committed - * room.afterSync(changes, preparation); + * room.afterSync(changes); * if (room.needsAfterSyncCompleted(changes)) { * // can do network requests * await room.afterSyncCompleted(changes); @@ -170,7 +170,7 @@ export class Sync { const isInitialSync = !syncToken; syncToken = response.next_batch; const roomStates = this._parseRoomsResponse(response.rooms, isInitialSync); - await this._prepareRooms(roomStates, isInitialSync); + await this._prepareRooms(roomStates); let sessionChanges; const syncTxn = await this._openSyncTxn(); try { From 64f657e5a214b3c4e9248810a42ff35f3ad3f6aa Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 23 Sep 2020 18:20:29 +0200 Subject: [PATCH 06/12] fix test --- src/matrix/room/RoomSummary.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index 392bf755..72769f34 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -291,10 +291,12 @@ export class RoomSummary { export function tests() { return { - "membership trigger change": function(assert) { + "membership trigger change": async function(assert) { const summary = new RoomSummary("id"); let written = false; - const changes = summary.writeSync({}, "join", false, false, {roomSummary: {set: () => { written = true; }}}); + let changes = summary.data.applySyncResponse({}, "join"); + const txn = {roomSummary: {set: () => { written = true; }}}; + changes = summary.writeData(changes, txn); assert(changes); assert(written); assert.equal(changes.membership, "join"); From 7cad3b2bdb32307898bee85e5cc4720f66b7f88d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 23 Sep 2020 18:22:51 +0200 Subject: [PATCH 07/12] some tabs in here --- src/matrix/room/Room.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index a0cea456..0ddff480 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -34,15 +34,15 @@ import {DecryptionSource} from "../e2ee/common.js"; const EVENT_ENCRYPTED_TYPE = "m.room.encrypted"; export class Room extends EventEmitter { - constructor({roomId, storage, hsApi, mediaRepository, emitCollectionChange, pendingEvents, user, createRoomEncryption, getSyncToken, clock}) { + constructor({roomId, storage, hsApi, mediaRepository, emitCollectionChange, pendingEvents, user, createRoomEncryption, getSyncToken, clock}) { super(); this._roomId = roomId; this._storage = storage; this._hsApi = hsApi; this._mediaRepository = mediaRepository; - this._summary = new RoomSummary(roomId); + this._summary = new RoomSummary(roomId); this._fragmentIdComparer = new FragmentIdComparer([]); - this._syncWriter = new SyncWriter({roomId, fragmentIdComparer: this._fragmentIdComparer}); + this._syncWriter = new SyncWriter({roomId, fragmentIdComparer: this._fragmentIdComparer}); this._emitCollectionChange = emitCollectionChange; this._sendQueue = new SendQueue({roomId, storage, hsApi, pendingEvents}); this._timeline = null; @@ -53,7 +53,7 @@ export class Room extends EventEmitter { this._roomEncryption = null; this._getSyncToken = getSyncToken; this._clock = clock; - } + } _readRetryDecryptCandidateEntries(sinceEventKey, txn) { if (sinceEventKey) { @@ -204,7 +204,7 @@ export class Room extends EventEmitter { /** @package */ async writeSync(roomResponse, isInitialSync, {summaryChanges, decryptChanges, roomEncryption}, txn) { - const {entries, newLiveKey, memberChanges} = + const {entries, newLiveKey, memberChanges} = await this._syncWriter.writeSync(roomResponse, txn); if (decryptChanges) { const decryption = await decryptChanges.write(txn); @@ -286,7 +286,7 @@ export class Room extends EventEmitter { if (removedPendingEvents) { this._sendQueue.emitRemovals(removedPendingEvents); } - } + } needsAfterSyncCompleted({memberChanges}) { return this._roomEncryption?.needsToShareKeys(memberChanges); @@ -321,7 +321,7 @@ export class Room extends EventEmitter { } /** @package */ - async load(summary, txn) { + async load(summary, txn) { try { this._summary.load(summary); if (this._summary.data.encryption) { @@ -338,7 +338,7 @@ export class Room extends EventEmitter { } catch (err) { throw new WrappedError(`Could not load room ${this._roomId}`, err); } - } + } /** @public */ sendEvent(eventType, content) { From 086bdafe9aeca3cf5eb6bbed08123f3b2c84157e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 23 Sep 2020 18:28:21 +0200 Subject: [PATCH 08/12] no need for async --- src/matrix/room/RoomSummary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index 72769f34..a6fda3a0 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -291,7 +291,7 @@ export class RoomSummary { export function tests() { return { - "membership trigger change": async function(assert) { + "membership trigger change": function(assert) { const summary = new RoomSummary("id"); let written = false; let changes = summary.data.applySyncResponse({}, "join"); From 17f84ab3148da348dc42fd4eea95c8a73107533f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 23 Sep 2020 19:09:44 +0200 Subject: [PATCH 09/12] fix "this" still being used in readRawTimelineEntries ... --- src/matrix/room/Room.js | 6 +++-- .../timeline/persistence/TimelineReader.js | 22 +++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 0ddff480..40173c3c 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -57,13 +57,15 @@ export class Room extends EventEmitter { _readRetryDecryptCandidateEntries(sinceEventKey, txn) { if (sinceEventKey) { - return readRawTimelineEntriesWithTxn(sinceEventKey, Direction.Forward, Number.MAX_SAFE_INTEGER, txn); + return readRawTimelineEntriesWithTxn(this._roomId, sinceEventKey, + Direction.Forward, Number.MAX_SAFE_INTEGER, this._fragmentIdComparer, txn); } else { // all messages for room ... // if you haven't decrypted any message in a room yet, // it's unlikely you will have tons of them. // so this should be fine as a last resort - return readRawTimelineEntriesWithTxn(this._syncWriter.lastMessageKey, Direction.Backward, Number.MAX_SAFE_INTEGER, txn); + return readRawTimelineEntriesWithTxn(this._roomId, this._syncWriter.lastMessageKey, + Direction.Backward, Number.MAX_SAFE_INTEGER, this._fragmentIdComparer, txn); } } diff --git a/src/matrix/room/timeline/persistence/TimelineReader.js b/src/matrix/room/timeline/persistence/TimelineReader.js index bf4d2766..37b15574 100644 --- a/src/matrix/room/timeline/persistence/TimelineReader.js +++ b/src/matrix/room/timeline/persistence/TimelineReader.js @@ -41,7 +41,7 @@ class ReaderRequest { * Raw because it doesn't do decryption and in the future it should not read relations either. * It is just about reading entries and following fragment links */ -export async function readRawTimelineEntriesWithTxn(eventKey, direction, amount, r, txn) { +export async function readRawTimelineEntriesWithTxn(roomId, eventKey, direction, amount, fragmentIdComparer, txn) { let entries = []; const timelineStore = txn.timelineEvents; const fragmentStore = txn.timelineFragments; @@ -50,29 +50,29 @@ export async function readRawTimelineEntriesWithTxn(eventKey, direction, amount, let eventsWithinFragment; if (direction.isForward) { // TODO: should we pass amount - entries.length here? - eventsWithinFragment = await timelineStore.eventsAfter(this._roomId, eventKey, amount); + eventsWithinFragment = await timelineStore.eventsAfter(roomId, eventKey, amount); } else { - eventsWithinFragment = await timelineStore.eventsBefore(this._roomId, eventKey, amount); + eventsWithinFragment = await timelineStore.eventsBefore(roomId, eventKey, amount); } - let eventEntries = eventsWithinFragment.map(e => new EventEntry(e, this._fragmentIdComparer)); + let eventEntries = eventsWithinFragment.map(e => new EventEntry(e, fragmentIdComparer)); entries = directionalConcat(entries, eventEntries, direction); // prepend or append eventsWithinFragment to entries, and wrap them in EventEntry if (entries.length < amount) { - const fragment = await fragmentStore.get(this._roomId, eventKey.fragmentId); + const fragment = await fragmentStore.get(roomId, eventKey.fragmentId); // TODO: why does the first fragment not need to be added? (the next *is* added below) // it looks like this would be fine when loading in the sync island // (as the live fragment should be added already) but not for permalinks when we support them // - // this._fragmentIdComparer.addFragment(fragment); - let fragmentEntry = new FragmentBoundaryEntry(fragment, direction.isBackward, this._fragmentIdComparer); + // fragmentIdComparer.addFragment(fragment); + let fragmentEntry = new FragmentBoundaryEntry(fragment, direction.isBackward, fragmentIdComparer); // append or prepend fragmentEntry, reuse func from GapWriter? directionalAppend(entries, fragmentEntry, direction); // only continue loading if the fragment boundary can't be backfilled if (!fragmentEntry.token && fragmentEntry.hasLinkedFragment) { - const nextFragment = await fragmentStore.get(this._roomId, fragmentEntry.linkedFragmentId); - this._fragmentIdComparer.add(nextFragment); - const nextFragmentEntry = new FragmentBoundaryEntry(nextFragment, direction.isForward, this._fragmentIdComparer); + const nextFragment = await fragmentStore.get(roomId, fragmentEntry.linkedFragmentId); + fragmentIdComparer.add(nextFragment); + const nextFragmentEntry = new FragmentBoundaryEntry(nextFragment, direction.isForward, fragmentIdComparer); directionalAppend(entries, nextFragmentEntry, direction); eventKey = nextFragmentEntry.asEventKey(); } else { @@ -133,7 +133,7 @@ export class TimelineReader { } async _readFrom(eventKey, direction, amount, r, txn) { - const entries = readRawTimelineEntriesWithTxn(eventKey, direction, amount, r, txn); + const entries = await readRawTimelineEntriesWithTxn(this._roomId, eventKey, direction, amount, this._fragmentIdComparer, txn); if (this._decryptEntries) { r.decryptRequest = this._decryptEntries(entries, txn); try { From eb4237f6f4d4e2c0daf686bf0980c554f87c436d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 23 Sep 2020 19:10:25 +0200 Subject: [PATCH 10/12] tell caller if an update was done from summery.writeAndApplyData so room actually emits an update --- src/matrix/room/RoomSummary.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index a6fda3a0..b4826fa3 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -262,7 +262,7 @@ export class RoomSummary { async writeAndApplyData(data, storage) { if (data === this._data) { - return; + return false; } const txn = await storage.readWriteTxn([ storage.storeNames.roomSummary, @@ -275,6 +275,7 @@ export class RoomSummary { } await txn.complete(); this.applyChanges(data); + return true; } applyChanges(data) { From ece4840653ce2c428cf83be6c11c1cdd47a6c564 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 23 Sep 2020 19:11:11 +0200 Subject: [PATCH 11/12] don't mark rooms as unread after retrying decryption for now this will not mark e2ee rooms as unread if their room key is delayed though. We should really only do this for back-filled events but that is hard to do right now, as we don't know the original source here. --- src/matrix/room/Room.js | 4 ++-- src/matrix/room/RoomSummary.js | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 40173c3c..369836ee 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -114,7 +114,7 @@ export class Room extends EventEmitter { // _decryptEntries entries and could even know which events have been decrypted for the first // time from DecryptionChanges.write and only pass those to the summary. As timeline changes // are not essential to the room summary, it's fine to write this in a separate txn for now. - const changes = this._summary.data.applyTimelineEntries(retryEntries, false, this._isTimelineOpen); + const changes = this._summary.data.applyTimelineEntries(retryEntries, false, false); if (await this._summary.writeAndApplyData(changes, this._storage)) { this._emitUpdate(); } @@ -218,7 +218,7 @@ export class Room extends EventEmitter { } // also apply (decrypted) timeline entries to the summary changes summaryChanges = summaryChanges.applyTimelineEntries( - entries, isInitialSync, this._isTimelineOpen, this._user.id); + entries, isInitialSync, !this._isTimelineOpen, this._user.id); // write summary changes, and unset if nothing was actually changed summaryChanges = this._summary.writeData(summaryChanges, txn); // fetch new members while we have txn open, diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index b4826fa3..c708ee91 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -17,11 +17,11 @@ limitations under the License. import {MEGOLM_ALGORITHM} from "../e2ee/common.js"; -function applyTimelineEntries(data, timelineEntries, isInitialSync, isTimelineOpen, ownUserId) { +function applyTimelineEntries(data, timelineEntries, isInitialSync, canMarkUnread, ownUserId) { if (timelineEntries.length) { data = timelineEntries.reduce((data, entry) => { return processTimelineEvent(data, entry, - isInitialSync, isTimelineOpen, ownUserId); + isInitialSync, canMarkUnread, ownUserId); }, data); } return data; @@ -105,13 +105,13 @@ function processStateEvent(data, event) { return data; } -function processTimelineEvent(data, eventEntry, isInitialSync, isTimelineOpen, ownUserId) { +function processTimelineEvent(data, eventEntry, isInitialSync, canMarkUnread, ownUserId) { if (eventEntry.eventType === "m.room.message") { if (!data.lastMessageTimestamp || eventEntry.timestamp > data.lastMessageTimestamp) { data = data.cloneIfNeeded(); data.lastMessageTimestamp = eventEntry.timestamp; } - if (!isInitialSync && eventEntry.sender !== ownUserId && !isTimelineOpen) { + if (!isInitialSync && eventEntry.sender !== ownUserId && canMarkUnread) { data = data.cloneIfNeeded(); data.isUnread = true; } @@ -208,8 +208,8 @@ class SummaryData { return serializedProps; } - applyTimelineEntries(timelineEntries, isInitialSync, isTimelineOpen, ownUserId) { - return applyTimelineEntries(this, timelineEntries, isInitialSync, isTimelineOpen, ownUserId); + applyTimelineEntries(timelineEntries, isInitialSync, canMarkUnread, ownUserId) { + return applyTimelineEntries(this, timelineEntries, isInitialSync, canMarkUnread, ownUserId); } applySyncResponse(roomResponse, membership) { From 4d616ce2812e7e9c2b6f497289111d45724ae37e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 24 Sep 2020 10:35:09 +0200 Subject: [PATCH 12/12] add todo for future optimisation --- src/matrix/e2ee/RoomEncryption.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index dac362c0..2c23933d 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -215,6 +215,8 @@ export class RoomEncryption { * @return {Array} the event ids that should be retried to decrypt */ getEventIdsForRoomKey(roomKey) { + // TODO: we could concat both results here, and only put stuff in + // candidates if it is not in missing sessions to use a bit less memory let eventIds = this._missingSessions.getEventIds(roomKey.senderKey, roomKey.sessionId); if (!eventIds) { eventIds = this._missingSessionCandidates.getEventIds(roomKey.senderKey, roomKey.sessionId);