also decrypt messages in the sync response that enabled encryption

like initial sync
This commit is contained in:
Bruno Windels 2020-09-23 14:26:14 +02:00
parent 241176d6fb
commit a8392dc684
5 changed files with 111 additions and 192 deletions

View File

@ -41,17 +41,14 @@ function timelineIsEmpty(roomResponse) {
/** /**
* Sync steps in js-pseudocode: * Sync steps in js-pseudocode:
* ```js * ```js
* let preparation;
* if (room.needsPrepareSync) {
* // can only read some stores * // can only read some stores
* preparation = await room.prepareSync(roomResponse, prepareTxn); * const preparation = await room.prepareSync(roomResponse, membership, prepareTxn);
* // can do async work that is not related to storage (such as decryption) * // can do async work that is not related to storage (such as decryption)
* preparation = await room.afterPrepareSync(preparation); * await room.afterPrepareSync(preparation);
* }
* // writes and calculates changes * // 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 * // applies and emits changes once syncTxn is committed
* room.afterSync(changes); * room.afterSync(changes, preparation);
* if (room.needsAfterSyncCompleted(changes)) { * if (room.needsAfterSyncCompleted(changes)) {
* // can do network requests * // can do network requests
* await room.afterSyncCompleted(changes); * await room.afterSyncCompleted(changes);
@ -173,14 +170,14 @@ export class Sync {
const isInitialSync = !syncToken; const isInitialSync = !syncToken;
syncToken = response.next_batch; syncToken = response.next_batch;
const roomStates = this._parseRoomsResponse(response.rooms, isInitialSync); const roomStates = this._parseRoomsResponse(response.rooms, isInitialSync);
await this._prepareRooms(roomStates); await this._prepareRooms(roomStates, isInitialSync);
let sessionChanges; let sessionChanges;
const syncTxn = await this._openSyncTxn(); const syncTxn = await this._openSyncTxn();
try { try {
await Promise.all(roomStates.map(async rs => { await Promise.all(roomStates.map(async rs => {
console.log(` * applying sync response to room ${rs.room.id} ...`); console.log(` * applying sync response to room ${rs.room.id} ...`);
rs.changes = await rs.room.writeSync( 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); sessionChanges = await this._session.writeSync(response, syncFilterId, syncTxn);
} catch(err) { } catch(err) {
@ -219,16 +216,11 @@ export class Sync {
} }
async _prepareRooms(roomStates) { async _prepareRooms(roomStates) {
const prepareRoomStates = roomStates.filter(rs => rs.room.needsPrepareSync);
if (prepareRoomStates.length) {
const prepareTxn = await this._openPrepareSyncTxn(); const prepareTxn = await this._openPrepareSyncTxn();
await Promise.all(prepareRoomStates.map(async rs => { await Promise.all(roomStates.map(async rs => {
rs.preparation = await rs.room.prepareSync(rs.roomResponse, prepareTxn); rs.preparation = await rs.room.prepareSync(rs.roomResponse, rs.membership, prepareTxn);
})); }));
await Promise.all(prepareRoomStates.map(async rs => { await Promise.all(roomStates.map(rs => rs.room.afterPrepareSync(rs.preparation)));
rs.preparation = await rs.room.afterPrepareSync(rs.preparation);
}));
}
} }
async _openSyncTxn() { async _openSyncTxn() {

View File

@ -37,7 +37,7 @@ export class Room extends EventEmitter {
this._storage = storage; this._storage = storage;
this._hsApi = hsApi; this._hsApi = hsApi;
this._mediaRepository = mediaRepository; this._mediaRepository = mediaRepository;
this._summary = new RoomSummary(roomId, user.id); this._summary = new RoomSummary(roomId);
this._fragmentIdComparer = new FragmentIdComparer([]); this._fragmentIdComparer = new FragmentIdComparer([]);
this._syncWriter = new SyncWriter({roomId, fragmentIdComparer: this._fragmentIdComparer}); this._syncWriter = new SyncWriter({roomId, fragmentIdComparer: this._fragmentIdComparer});
this._emitCollectionChange = emitCollectionChange; 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 // _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 // 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. // 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); const changes = this._summary.data.applyTimelineEntries(retryEntries, false, this._isTimelineOpen);
if (changes) { if (await this._summary.writeAndApplyData(changes, this._storage)) {
this._summary.writeAndApplyChanges(changes, this._storage);
this._emitUpdate(); this._emitUpdate();
} }
} }
} }
} }
_enableEncryption(encryptionParams) { _setEncryption(roomEncryption) {
this._roomEncryption = this._createRoomEncryption(this, encryptionParams); if (roomEncryption && !this._roomEncryption) {
if (this._roomEncryption) { this._roomEncryption = roomEncryption;
this._sendQueue.enableEncryption(this._roomEncryption); this._sendQueue.enableEncryption(this._roomEncryption);
if (this._timeline) { if (this._timeline) {
this._timeline.enableEncryption(this._decryptEntries.bind(this, DecryptionSource.Timeline)); this._timeline.enableEncryption(this._decryptEntries.bind(this, DecryptionSource.Timeline));
@ -141,57 +140,62 @@ export class Room extends EventEmitter {
return request; return request;
} }
get needsPrepareSync() { async prepareSync(roomResponse, membership, txn) {
// only encrypted rooms need the prepare sync steps const summaryChanges = this._summary.data.applySyncResponse(roomResponse, membership)
return !!this._roomEncryption; let roomEncryption = this._roomEncryption;
// encryption is enabled in this sync
if (!roomEncryption && summaryChanges.encryption) {
roomEncryption = this._createRoomEncryption(this, summaryChanges.encryption);
} }
async prepareSync(roomResponse, txn) { let decryptPreparation;
if (this._roomEncryption) { if (roomEncryption) {
const events = roomResponse?.timeline?.events; const events = roomResponse?.timeline?.events;
if (Array.isArray(events)) { if (Array.isArray(events)) {
const eventsToDecrypt = events.filter(event => { const eventsToDecrypt = events.filter(event => {
return event?.type === EVENT_ENCRYPTED_TYPE; return event?.type === EVENT_ENCRYPTED_TYPE;
}); });
const preparation = await this._roomEncryption.prepareDecryptAll( decryptPreparation = await roomEncryption.prepareDecryptAll(
eventsToDecrypt, DecryptionSource.Sync, this._isTimelineOpen, txn); eventsToDecrypt, DecryptionSource.Sync, this._isTimelineOpen, txn);
return preparation;
}
} }
} }
return {
roomEncryption,
summaryChanges,
decryptPreparation,
decryptChanges: null,
};
}
async afterPrepareSync(preparation) { async afterPrepareSync(preparation) {
if (preparation) { if (preparation.decryptPreparation) {
const decryptChanges = await preparation.decrypt(); preparation.decryptChanges = await preparation.decryptPreparation.decrypt();
return decryptChanges; preparation.decryptPreparation = null;
} }
} }
/** @package */ /** @package */
async writeSync(roomResponse, membership, isInitialSync, decryptChanges, txn) { async writeSync(roomResponse, isInitialSync, {summaryChanges, decryptChanges, roomEncryption}, txn) {
let decryption;
if (this._roomEncryption && decryptChanges) {
decryption = await decryptChanges.write(txn);
}
const {entries, newLiveKey, memberChanges} = const {entries, newLiveKey, memberChanges} =
await this._syncWriter.writeSync(roomResponse, txn); await this._syncWriter.writeSync(roomResponse, txn);
if (decryption) { if (decryptChanges) {
const decryption = await decryptChanges.write(txn);
decryption.applyToEntries(entries); decryption.applyToEntries(entries);
} }
// pass member changes to device tracker // pass member changes to device tracker
if (this._roomEncryption && this.isTrackingMembers && memberChanges?.size) { if (roomEncryption && this.isTrackingMembers && memberChanges?.size) {
await this._roomEncryption.writeMemberChanges(memberChanges, txn); await roomEncryption.writeMemberChanges(memberChanges, txn);
} }
const summaryChanges = this._summary.writeSync( // also apply (decrypted) timeline entries to the summary changes
roomResponse, summaryChanges = summaryChanges.applyTimelineEntries(
entries, entries, isInitialSync, this._isTimelineOpen, this._user.id);
membership, // write summary changes, and unset if nothing was actually changed
isInitialSync, this._isTimelineOpen, summaryChanges = this._summary.writeData(summaryChanges, txn);
txn);
// fetch new members while we have txn open, // fetch new members while we have txn open,
// but don't make any in-memory changes yet // but don't make any in-memory changes yet
let heroChanges; let heroChanges;
if (summaryChanges && needsHeroes(summaryChanges)) { if (summaryChanges?.needsHeroes) {
// room name disappeared, open heroes // room name disappeared, open heroes
if (!this._heroes) { if (!this._heroes) {
this._heroes = new Heroes(this._roomId); this._heroes = new Heroes(this._roomId);
@ -204,6 +208,7 @@ export class Room extends EventEmitter {
} }
return { return {
summaryChanges, summaryChanges,
roomEncryption,
newTimelineEntries: entries, newTimelineEntries: entries,
newLiveKey, newLiveKey,
removedPendingEvents, removedPendingEvents,
@ -217,11 +222,9 @@ export class Room extends EventEmitter {
* Called with the changes returned from `writeSync` to apply them and emit changes. * Called with the changes returned from `writeSync` to apply them and emit changes.
* No storage or network operations should be done here. * 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); this._syncWriter.afterSync(newLiveKey);
if (!this._summary.encryption && summaryChanges.encryption && !this._roomEncryption) { this._setEncryption(roomEncryption);
this._enableEncryption(summaryChanges.encryption);
}
if (memberChanges.size) { if (memberChanges.size) {
if (this._changedMembersDuringSync) { if (this._changedMembersDuringSync) {
for (const [userId, memberChange] of memberChanges.entries()) { for (const [userId, memberChange] of memberChanges.entries()) {
@ -235,14 +238,14 @@ export class Room extends EventEmitter {
let emitChange = false; let emitChange = false;
if (summaryChanges) { if (summaryChanges) {
this._summary.applyChanges(summaryChanges); this._summary.applyChanges(summaryChanges);
if (!this._summary.needsHeroes) { if (!this._summary.data.needsHeroes) {
this._heroes = null; this._heroes = null;
} }
emitChange = true; emitChange = true;
} }
if (this._heroes && heroChanges) { if (this._heroes && heroChanges) {
const oldName = this.name; const oldName = this.name;
this._heroes.applyChanges(heroChanges, this._summary); this._heroes.applyChanges(heroChanges, this._summary.data);
if (oldName !== this.name) { if (oldName !== this.name) {
emitChange = true; emitChange = true;
} }
@ -294,14 +297,15 @@ export class Room extends EventEmitter {
async load(summary, txn) { async load(summary, txn) {
try { try {
this._summary.load(summary); this._summary.load(summary);
if (this._summary.encryption) { if (this._summary.data.encryption) {
this._enableEncryption(this._summary.encryption); const roomEncryption = this._createRoomEncryption(this, this._summary.data.encryption);
this._setEncryption(roomEncryption);
} }
// need to load members for name? // need to load members for name?
if (this._summary.needsHeroes) { if (this._summary.data.needsHeroes) {
this._heroes = new Heroes(this._roomId); this._heroes = new Heroes(this._roomId);
const changes = await this._heroes.calculateChanges(this._summary.heroes, [], txn); const changes = await this._heroes.calculateChanges(this._summary.data.heroes, [], txn);
this._heroes.applyChanges(changes, this._summary); this._heroes.applyChanges(changes, this._summary.data);
} }
return this._syncWriter.load(txn); return this._syncWriter.load(txn);
} catch (err) { } catch (err) {
@ -397,7 +401,14 @@ export class Room extends EventEmitter {
if (this._heroes) { if (this._heroes) {
return this._heroes.roomName; 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 */ /** @public */
@ -406,8 +417,8 @@ export class Room extends EventEmitter {
} }
get avatarUrl() { get avatarUrl() {
if (this._summary.avatarUrl) { if (this._summary.data.avatarUrl) {
return this._summary.avatarUrl; return this._summary.data.avatarUrl;
} else if (this._heroes) { } else if (this._heroes) {
return this._heroes.roomAvatarUrl; return this._heroes.roomAvatarUrl;
} }
@ -415,28 +426,28 @@ export class Room extends EventEmitter {
} }
get lastMessageTimestamp() { get lastMessageTimestamp() {
return this._summary.lastMessageTimestamp; return this._summary.data.lastMessageTimestamp;
} }
get isUnread() { get isUnread() {
return this._summary.isUnread; return this._summary.data.isUnread;
} }
get notificationCount() { get notificationCount() {
return this._summary.notificationCount; return this._summary.data.notificationCount;
} }
get highlightCount() { get highlightCount() {
return this._summary.highlightCount; return this._summary.data.highlightCount;
} }
get isLowPriority() { get isLowPriority() {
const tags = this._summary.tags; const tags = this._summary.data.tags;
return !!(tags && tags['m.lowpriority']); return !!(tags && tags['m.lowpriority']);
} }
get isEncrypted() { get isEncrypted() {
return !!this._summary.encryption; return !!this._summary.data.encryption;
} }
enableSessionBackup(sessionBackup) { enableSessionBackup(sessionBackup) {
@ -444,7 +455,7 @@ export class Room extends EventEmitter {
} }
get isTrackingMembers() { get isTrackingMembers() {
return this._summary.isTrackingMembers; return this._summary.data.isTrackingMembers;
} }
async _getLastEventId() { async _getLastEventId() {

View File

@ -39,16 +39,17 @@ function applySyncResponse(data, roomResponse, membership) {
if (roomResponse.account_data) { if (roomResponse.account_data) {
data = roomResponse.account_data.events.reduce(processRoomAccountData, data); data = roomResponse.account_data.events.reduce(processRoomAccountData, data);
} }
const stateEvents = roomResponse?.state?.events;
// state comes before timeline // state comes before timeline
if (roomResponse.state) { if (Array.isArray(stateEvents)) {
data = roomResponse.state.events.reduce(processStateEvent, data); data = stateEvents.reduce(processStateEvent, data);
} }
const {timeline} = roomResponse; const timelineEvents = roomResponse?.timeline?.events;
// process state events in timeline // process state events in timeline
// non-state events are handled by applyTimelineEntries // non-state events are handled by applyTimelineEntries
// so decryption is handled properly // so decryption is handled properly
if (timeline && Array.isArray(timeline.events)) { if (Array.isArray(timelineEvents)) {
data = timeline.events.reduce((data, event) => { data = timelineEvents.reduce((data, event) => {
if (typeof event.state_key === "string") { if (typeof event.state_key === "string") {
return processStateEvent(data, event); return processStateEvent(data, event);
} }
@ -200,87 +201,27 @@ class SummaryData {
const {cloned, ...serializedProps} = this; const {cloned, ...serializedProps} = this;
return serializedProps; return serializedProps;
} }
applyTimelineEntries(timelineEntries, isInitialSync, isTimelineOpen, ownUserId) {
return applyTimelineEntries(this, timelineEntries, isInitialSync, isTimelineOpen, ownUserId);
} }
export function needsHeroes(data) { applySyncResponse(roomResponse, membership) {
return !data.name && !data.canonicalAlias && data.heroes && data.heroes.length > 0; return applySyncResponse(this, roomResponse, membership);
}
get needsHeroes() {
return !this.name && !this.canonicalAlias && this.heroes && this.heroes.length > 0;
}
} }
export class RoomSummary { export class RoomSummary {
constructor(roomId, ownUserId) { constructor(roomId) {
this._ownUserId = ownUserId;
this._data = new SummaryData(null, roomId); this._data = new SummaryData(null, roomId);
} }
get name() { get data() {
if (this._data.name) { return this._data;
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;
} }
writeClearUnread(txn) { writeClearUnread(txn) {
@ -306,45 +247,17 @@ export class RoomSummary {
return data; return data;
} }
/** writeData(data, txn) {
* 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);
if (data !== this._data) { if (data !== this._data) {
txn.roomSummary.set(data.serialize()); txn.roomSummary.set(data.serialize());
return data; return data;
} }
} }
/** async writeAndApplyData(data, storage) {
* Only to be used with processTimelineEntries, if (data === this._data) {
* other methods like writeSync, writeHasFetchedMembers, return;
* writeIsTrackingMembers, ... take a txn directly. }
*/
async writeAndApplyChanges(data, storage) {
const txn = await storage.readWriteTxn([ const txn = await storage.readWriteTxn([
storage.storeNames.roomSummary, storage.storeNames.roomSummary,
]); ]);
@ -360,6 +273,9 @@ export class RoomSummary {
applyChanges(data) { applyChanges(data) {
this._data = 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) { async load(summary) {

View File

@ -16,8 +16,8 @@ limitations under the License.
import {RoomMember} from "./RoomMember.js"; import {RoomMember} from "./RoomMember.js";
function calculateRoomName(sortedMembers, summary) { function calculateRoomName(sortedMembers, summaryData) {
const countWithoutMe = summary.joinCount + summary.inviteCount - 1; const countWithoutMe = summaryData.joinCount + summaryData.inviteCount - 1;
if (sortedMembers.length >= countWithoutMe) { if (sortedMembers.length >= countWithoutMe) {
if (sortedMembers.length > 1) { if (sortedMembers.length > 1) {
const lastMember = sortedMembers[sortedMembers.length - 1]; const lastMember = sortedMembers[sortedMembers.length - 1];
@ -74,7 +74,7 @@ export class Heroes {
return {updatedHeroMembers: updatedHeroMembers.values(), removedUserIds}; return {updatedHeroMembers: updatedHeroMembers.values(), removedUserIds};
} }
applyChanges({updatedHeroMembers, removedUserIds}, summary) { applyChanges({updatedHeroMembers, removedUserIds}, summaryData) {
for (const userId of removedUserIds) { for (const userId of removedUserIds) {
this._members.delete(userId); this._members.delete(userId);
} }
@ -82,7 +82,7 @@ export class Heroes {
this._members.set(member.userId, member); this._members.set(member.userId, member);
} }
const sortedMembers = Array.from(this._members.values()).sort((a, b) => a.name.localeCompare(b.name)); 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() { get roomName() {

View File

@ -82,7 +82,7 @@ async function fetchMembers({summary, syncToken, roomId, hsApi, storage, setChan
export async function fetchOrLoadMembers(options) { export async function fetchOrLoadMembers(options) {
const {summary} = options; const {summary} = options;
if (!summary.hasFetchedMembers) { if (!summary.data.hasFetchedMembers) {
return fetchMembers(options); return fetchMembers(options);
} else { } else {
return loadMembers(options); return loadMembers(options);