2020-08-05 18:38:55 +02:00
|
|
|
/*
|
|
|
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2020-04-20 21:35:53 +02:00
|
|
|
import {EventEmitter} from "../../utils/EventEmitter.js";
|
2020-09-23 17:59:42 +02:00
|
|
|
import {RoomSummary} from "./RoomSummary.js";
|
2020-04-20 21:26:39 +02:00
|
|
|
import {SyncWriter} from "./timeline/persistence/SyncWriter.js";
|
|
|
|
import {GapWriter} from "./timeline/persistence/GapWriter.js";
|
|
|
|
import {Timeline} from "./timeline/Timeline.js";
|
|
|
|
import {FragmentIdComparer} from "./timeline/FragmentIdComparer.js";
|
|
|
|
import {SendQueue} from "./sending/SendQueue.js";
|
2020-08-17 10:48:00 +02:00
|
|
|
import {WrappedError} from "../error.js"
|
2020-08-19 16:58:19 +02:00
|
|
|
import {fetchOrLoadMembers} from "./members/load.js";
|
2020-08-19 16:29:54 +02:00
|
|
|
import {MemberList} from "./members/MemberList.js";
|
2020-08-21 18:11:07 +02:00
|
|
|
import {Heroes} from "./members/Heroes.js";
|
2020-09-04 15:28:22 +02:00
|
|
|
import {EventEntry} from "./timeline/entries/EventEntry.js";
|
2020-10-30 15:19:51 +01:00
|
|
|
import {ObservedEventMap} from "./ObservedEventMap.js";
|
2020-11-11 10:47:55 +01:00
|
|
|
import {AttachmentUpload} from "./AttachmentUpload.js";
|
2020-09-10 12:09:17 +02:00
|
|
|
import {DecryptionSource} from "../e2ee/common.js";
|
2021-03-15 12:56:40 +01:00
|
|
|
import {ensureLogItem} from "../../logging/utils.js";
|
2020-11-11 10:47:55 +01:00
|
|
|
|
2020-09-10 12:09:17 +02:00
|
|
|
const EVENT_ENCRYPTED_TYPE = "m.room.encrypted";
|
2018-12-21 14:35:24 +01:00
|
|
|
|
2020-04-20 21:26:39 +02:00
|
|
|
export class Room extends EventEmitter {
|
2020-11-11 10:47:19 +01:00
|
|
|
constructor({roomId, storage, hsApi, mediaRepository, emitCollectionChange, pendingEvents, user, createRoomEncryption, getSyncToken, platform}) {
|
2019-02-20 23:48:16 +01:00
|
|
|
super();
|
2019-03-08 20:03:18 +01:00
|
|
|
this._roomId = roomId;
|
|
|
|
this._storage = storage;
|
|
|
|
this._hsApi = hsApi;
|
2020-09-22 13:40:38 +02:00
|
|
|
this._mediaRepository = mediaRepository;
|
2020-09-23 18:22:51 +02:00
|
|
|
this._summary = new RoomSummary(roomId);
|
2019-05-12 20:24:06 +02:00
|
|
|
this._fragmentIdComparer = new FragmentIdComparer([]);
|
2020-09-23 18:22:51 +02:00
|
|
|
this._syncWriter = new SyncWriter({roomId, fragmentIdComparer: this._fragmentIdComparer});
|
2019-02-20 23:48:16 +01:00
|
|
|
this._emitCollectionChange = emitCollectionChange;
|
2020-09-22 13:43:18 +02:00
|
|
|
this._sendQueue = new SendQueue({roomId, storage, hsApi, pendingEvents});
|
2019-02-27 22:50:08 +01:00
|
|
|
this._timeline = null;
|
2019-07-29 10:23:15 +02:00
|
|
|
this._user = user;
|
2020-08-19 16:12:49 +02:00
|
|
|
this._changedMembersDuringSync = null;
|
2020-08-31 08:54:27 +02:00
|
|
|
this._memberList = null;
|
2020-09-03 15:36:17 +02:00
|
|
|
this._createRoomEncryption = createRoomEncryption;
|
|
|
|
this._roomEncryption = null;
|
2020-09-09 09:50:03 +02:00
|
|
|
this._getSyncToken = getSyncToken;
|
2020-11-11 10:47:19 +01:00
|
|
|
this._platform = platform;
|
2020-10-30 15:19:51 +01:00
|
|
|
this._observedEvents = null;
|
2020-09-23 18:22:51 +02:00
|
|
|
}
|
2018-12-21 14:35:24 +01:00
|
|
|
|
2021-03-15 13:38:27 +01:00
|
|
|
async _eventIdsToEntries(eventIds, txn) {
|
2021-03-02 15:29:35 +01:00
|
|
|
const retryEntries = [];
|
2021-03-15 13:38:27 +01:00
|
|
|
await Promise.all(eventIds.map(async eventId => {
|
|
|
|
const storageEntry = await txn.timelineEvents.getByEventId(this._roomId, eventId);
|
|
|
|
if (storageEntry) {
|
|
|
|
retryEntries.push(new EventEntry(storageEntry, this._fragmentIdComparer));
|
2021-03-02 15:29:35 +01:00
|
|
|
}
|
2021-03-15 13:38:27 +01:00
|
|
|
}));
|
2021-03-02 15:29:35 +01:00
|
|
|
return retryEntries;
|
2020-09-22 18:22:37 +02:00
|
|
|
}
|
|
|
|
|
2021-03-15 14:33:14 +01:00
|
|
|
_getAdditionalTimelineRetryEntries(otherRetryEntries, roomKeys) {
|
|
|
|
let retryTimelineEntries = this._roomEncryption.filterUndecryptedEventEntriesForKeys(this._timeline.remoteEntries, roomKeys);
|
|
|
|
// filter out any entries already in retryEntries so we don't decrypt them twice
|
|
|
|
const existingIds = otherRetryEntries.reduce((ids, e) => {ids.add(e.id); return ids;}, new Set());
|
|
|
|
retryTimelineEntries = retryTimelineEntries.filter(e => !existingIds.has(e.id));
|
|
|
|
return retryTimelineEntries;
|
|
|
|
}
|
|
|
|
|
2021-03-02 15:29:35 +01:00
|
|
|
/**
|
2021-03-15 13:38:27 +01:00
|
|
|
* Used for retrying decryption from other sources than sync, like key backup.
|
2021-03-02 15:29:35 +01:00
|
|
|
* @internal
|
2021-03-15 14:33:14 +01:00
|
|
|
* @param {RoomKey} roomKey
|
|
|
|
* @param {Array<string>} eventIds any event ids that should be retried. There might be more in the timeline though for this key.
|
2021-03-02 15:29:35 +01:00
|
|
|
* @return {Promise}
|
|
|
|
*/
|
2021-03-15 12:56:40 +01:00
|
|
|
async notifyRoomKey(roomKey, eventIds, log) {
|
2020-09-23 17:34:25 +02:00
|
|
|
if (!this._roomEncryption) {
|
|
|
|
return;
|
|
|
|
}
|
2021-03-04 19:47:02 +01:00
|
|
|
const txn = await this._storage.readTxn([
|
2020-09-23 17:34:25 +02:00
|
|
|
this._storage.storeNames.timelineEvents,
|
|
|
|
this._storage.storeNames.inboundGroupSessions,
|
2021-03-02 15:29:35 +01:00
|
|
|
]);
|
2021-03-15 14:33:14 +01:00
|
|
|
let retryEntries = await this._eventIdsToEntries(eventIds, txn);
|
|
|
|
if (this._timeline) {
|
|
|
|
const retryTimelineEntries = this._getAdditionalTimelineRetryEntries(retryEntries, [roomKey]);
|
|
|
|
retryEntries = retryEntries.concat(retryTimelineEntries);
|
|
|
|
}
|
2021-03-02 15:29:35 +01:00
|
|
|
if (retryEntries.length) {
|
2021-03-15 12:56:40 +01:00
|
|
|
const decryptRequest = this._decryptEntries(DecryptionSource.Retry, retryEntries, txn, log);
|
2020-09-23 17:34:25 +02:00
|
|
|
// 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.
|
2020-09-23 19:11:11 +02:00
|
|
|
const changes = this._summary.data.applyTimelineEntries(retryEntries, false, false);
|
2020-09-23 17:34:25 +02:00
|
|
|
if (await this._summary.writeAndApplyData(changes, this._storage)) {
|
|
|
|
this._emitUpdate();
|
|
|
|
}
|
2020-09-04 12:10:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-23 14:26:14 +02:00
|
|
|
_setEncryption(roomEncryption) {
|
|
|
|
if (roomEncryption && !this._roomEncryption) {
|
|
|
|
this._roomEncryption = roomEncryption;
|
2020-09-04 15:28:22 +02:00
|
|
|
this._sendQueue.enableEncryption(this._roomEncryption);
|
2020-09-04 16:27:39 +02:00
|
|
|
if (this._timeline) {
|
2020-09-10 12:09:17 +02:00
|
|
|
this._timeline.enableEncryption(this._decryptEntries.bind(this, DecryptionSource.Timeline));
|
2020-09-04 16:27:39 +02:00
|
|
|
}
|
2020-09-04 15:28:22 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-10 12:09:17 +02:00
|
|
|
/**
|
|
|
|
* Used for decrypting when loading/filling the timeline, and retrying decryption,
|
|
|
|
* not during sync, where it is split up during the multiple phases.
|
|
|
|
*/
|
2021-03-15 15:23:35 +01:00
|
|
|
_decryptEntries(source, entries, inboundSessionTxn, log = null) {
|
2021-03-15 12:56:40 +01:00
|
|
|
const request = new DecryptionRequest(async (r, log) => {
|
2020-09-10 16:40:30 +02:00
|
|
|
if (!inboundSessionTxn) {
|
2021-03-04 19:47:02 +01:00
|
|
|
inboundSessionTxn = await this._storage.readTxn([this._storage.storeNames.inboundGroupSessions]);
|
2020-09-10 16:40:30 +02:00
|
|
|
}
|
|
|
|
if (r.cancelled) return;
|
|
|
|
const events = entries.filter(entry => {
|
|
|
|
return entry.eventType === EVENT_ENCRYPTED_TYPE;
|
|
|
|
}).map(entry => entry.event);
|
2021-03-01 23:14:14 +01:00
|
|
|
r.preparation = await this._roomEncryption.prepareDecryptAll(events, null, source, inboundSessionTxn);
|
2020-09-10 16:40:30 +02:00
|
|
|
if (r.cancelled) return;
|
|
|
|
const changes = await r.preparation.decrypt();
|
|
|
|
r.preparation = null;
|
|
|
|
if (r.cancelled) return;
|
|
|
|
const stores = [this._storage.storeNames.groupSessionDecryptions];
|
2021-03-01 23:14:14 +01:00
|
|
|
const isTimelineOpen = this._isTimelineOpen;
|
2020-09-10 16:40:30 +02:00
|
|
|
if (isTimelineOpen) {
|
|
|
|
// read to fetch devices if timeline is open
|
|
|
|
stores.push(this._storage.storeNames.deviceIdentities);
|
|
|
|
}
|
2021-03-04 19:47:02 +01:00
|
|
|
const writeTxn = await this._storage.readWriteTxn(stores);
|
2020-09-10 16:40:30 +02:00
|
|
|
let decryption;
|
|
|
|
try {
|
2021-03-15 12:56:40 +01:00
|
|
|
decryption = await changes.write(writeTxn, log);
|
2021-03-01 23:14:14 +01:00
|
|
|
if (isTimelineOpen) {
|
|
|
|
await decryption.verifySenders(writeTxn);
|
|
|
|
}
|
2020-09-10 16:40:30 +02:00
|
|
|
} catch (err) {
|
|
|
|
writeTxn.abort();
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
await writeTxn.complete();
|
2021-02-23 19:22:25 +01:00
|
|
|
// TODO: log decryption errors here
|
2020-09-10 16:40:30 +02:00
|
|
|
decryption.applyToEntries(entries);
|
2020-10-30 15:19:51 +01:00
|
|
|
if (this._observedEvents) {
|
|
|
|
this._observedEvents.updateEvents(entries);
|
|
|
|
}
|
2021-03-15 15:23:46 +01:00
|
|
|
}, ensureLogItem(log));
|
2020-09-10 16:40:30 +02:00
|
|
|
return request;
|
2020-09-10 12:09:17 +02:00
|
|
|
}
|
|
|
|
|
2021-03-05 11:50:54 +01:00
|
|
|
async _getSyncRetryDecryptEntries(newKeys, roomEncryption, txn) {
|
2021-03-15 13:38:27 +01:00
|
|
|
const entriesPerKey = await Promise.all(newKeys.map(async key => {
|
|
|
|
const retryEventIds = await roomEncryption.getEventIdsForMissingKey(key, txn);
|
|
|
|
if (retryEventIds) {
|
|
|
|
return this._eventIdsToEntries(retryEventIds, txn);
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
let retryEntries = entriesPerKey.reduce((allEntries, entries) => entries ? allEntries.concat(entries) : allEntries, []);
|
2021-03-03 13:53:52 +01:00
|
|
|
// If we have the timeline open, see if there are more entries for the new keys
|
|
|
|
// as we only store missing session information for synced events, not backfilled.
|
|
|
|
// We want to decrypt all events we can though if the user is looking
|
|
|
|
// at them when the timeline is open
|
|
|
|
if (this._timeline) {
|
2021-03-15 14:33:14 +01:00
|
|
|
const retryTimelineEntries = this._getAdditionalTimelineRetryEntries(retryEntries, newKeys);
|
2021-03-03 13:53:52 +01:00
|
|
|
// make copies so we don't modify the original entry in writeSync, before the afterSync stage
|
|
|
|
const retryTimelineEntriesCopies = retryTimelineEntries.map(e => e.clone());
|
|
|
|
// add to other retry entries
|
|
|
|
retryEntries = retryEntries.concat(retryTimelineEntriesCopies);
|
2021-03-03 11:27:55 +01:00
|
|
|
}
|
2021-03-03 13:53:52 +01:00
|
|
|
return retryEntries;
|
2021-03-03 11:27:55 +01:00
|
|
|
}
|
|
|
|
|
2021-04-20 17:39:46 +02:00
|
|
|
async prepareSync(roomResponse, membership, invite, newKeys, txn, log) {
|
2021-02-17 18:45:04 +01:00
|
|
|
log.set("id", this.id);
|
2021-03-01 15:04:45 +01:00
|
|
|
if (newKeys) {
|
|
|
|
log.set("newKeys", newKeys.length);
|
|
|
|
}
|
2021-04-20 17:39:46 +02:00
|
|
|
let summaryChanges = this._summary.data.applySyncResponse(roomResponse, membership);
|
|
|
|
if (invite) {
|
|
|
|
summaryChanges = summaryChanges.applyInvite(invite);
|
|
|
|
}
|
2020-09-23 14:26:14 +02:00
|
|
|
let roomEncryption = this._roomEncryption;
|
|
|
|
// encryption is enabled in this sync
|
|
|
|
if (!roomEncryption && summaryChanges.encryption) {
|
2021-02-17 18:45:04 +01:00
|
|
|
log.set("enableEncryption", true);
|
2020-09-23 14:26:14 +02:00
|
|
|
roomEncryption = this._createRoomEncryption(this, summaryChanges.encryption);
|
|
|
|
}
|
2020-09-10 12:11:43 +02:00
|
|
|
|
2021-03-01 22:30:33 +01:00
|
|
|
let retryEntries;
|
2020-09-23 14:26:14 +02:00
|
|
|
let decryptPreparation;
|
|
|
|
if (roomEncryption) {
|
2021-03-03 13:53:52 +01:00
|
|
|
let eventsToDecrypt = roomResponse?.timeline?.events || [];
|
|
|
|
// when new keys arrive, also see if any older events can now be retried to decrypt
|
|
|
|
if (newKeys) {
|
2021-03-05 11:50:54 +01:00
|
|
|
retryEntries = await this._getSyncRetryDecryptEntries(newKeys, roomEncryption, txn);
|
2021-03-03 13:53:52 +01:00
|
|
|
if (retryEntries.length) {
|
|
|
|
log.set("retry", retryEntries.length);
|
|
|
|
eventsToDecrypt = eventsToDecrypt.concat(retryEntries.map(entry => entry.event));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
eventsToDecrypt = eventsToDecrypt.filter(event => {
|
|
|
|
return event?.type === EVENT_ENCRYPTED_TYPE;
|
|
|
|
});
|
|
|
|
if (eventsToDecrypt.length) {
|
|
|
|
decryptPreparation = await roomEncryption.prepareDecryptAll(
|
|
|
|
eventsToDecrypt, newKeys, DecryptionSource.Sync, txn);
|
|
|
|
}
|
2020-09-04 15:28:22 +02:00
|
|
|
}
|
2020-09-23 14:26:14 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
roomEncryption,
|
|
|
|
summaryChanges,
|
|
|
|
decryptPreparation,
|
|
|
|
decryptChanges: null,
|
2021-03-01 22:30:33 +01:00
|
|
|
retryEntries
|
2020-09-23 14:26:14 +02:00
|
|
|
};
|
2020-09-04 15:28:22 +02:00
|
|
|
}
|
|
|
|
|
2021-02-17 18:45:04 +01:00
|
|
|
async afterPrepareSync(preparation, parentLog) {
|
2020-09-23 14:26:14 +02:00
|
|
|
if (preparation.decryptPreparation) {
|
2021-03-02 15:29:35 +01:00
|
|
|
await parentLog.wrap("decrypt", async log => {
|
2021-02-17 18:45:04 +01:00
|
|
|
log.set("id", this.id);
|
|
|
|
preparation.decryptChanges = await preparation.decryptPreparation.decrypt();
|
|
|
|
preparation.decryptPreparation = null;
|
2021-02-18 12:39:29 +01:00
|
|
|
}, parentLog.level.Detail);
|
2020-09-10 12:11:43 +02:00
|
|
|
}
|
2020-09-04 12:09:19 +02:00
|
|
|
}
|
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @package */
|
2021-03-01 22:30:33 +01:00
|
|
|
async writeSync(roomResponse, isInitialSync, {summaryChanges, decryptChanges, roomEncryption, retryEntries}, txn, log) {
|
2021-02-17 18:45:04 +01:00
|
|
|
log.set("id", this.id);
|
2021-03-03 11:27:18 +01:00
|
|
|
const {entries: newEntries, newLiveKey, memberChanges} =
|
2021-02-19 11:57:17 +01:00
|
|
|
await log.wrap("syncWriter", log => this._syncWriter.writeSync(roomResponse, txn, log), log.level.Detail);
|
2021-03-03 11:27:18 +01:00
|
|
|
let allEntries = newEntries;
|
2020-09-23 14:26:14 +02:00
|
|
|
if (decryptChanges) {
|
2021-03-15 12:56:40 +01:00
|
|
|
const decryption = await log.wrap("decryptChanges", log => decryptChanges.write(txn, log));
|
2021-03-02 21:29:32 +01:00
|
|
|
log.set("decryptionResults", decryption.results.size);
|
|
|
|
log.set("decryptionErrors", decryption.errors.size);
|
2021-03-01 23:14:14 +01:00
|
|
|
if (this._isTimelineOpen) {
|
|
|
|
await decryption.verifySenders(txn);
|
|
|
|
}
|
2021-03-03 11:27:18 +01:00
|
|
|
decryption.applyToEntries(newEntries);
|
2021-03-01 22:30:33 +01:00
|
|
|
if (retryEntries?.length) {
|
2021-03-03 11:27:18 +01:00
|
|
|
decryption.applyToEntries(retryEntries);
|
|
|
|
allEntries = retryEntries.concat(allEntries);
|
2021-03-01 22:30:33 +01:00
|
|
|
}
|
2020-09-10 12:11:43 +02:00
|
|
|
}
|
2021-03-03 11:27:18 +01:00
|
|
|
log.set("allEntries", allEntries.length);
|
2021-03-02 19:14:29 +01:00
|
|
|
let shouldFlushKeyShares = false;
|
2020-09-10 12:11:43 +02:00
|
|
|
// pass member changes to device tracker
|
2020-09-23 14:26:14 +02:00
|
|
|
if (roomEncryption && this.isTrackingMembers && memberChanges?.size) {
|
2021-03-03 11:51:50 +01:00
|
|
|
shouldFlushKeyShares = await roomEncryption.writeMemberChanges(memberChanges, txn, log);
|
2021-03-02 19:14:29 +01:00
|
|
|
log.set("shouldFlushKeyShares", shouldFlushKeyShares);
|
2020-09-10 12:11:43 +02:00
|
|
|
}
|
2020-09-23 14:26:14 +02:00
|
|
|
// also apply (decrypted) timeline entries to the summary changes
|
|
|
|
summaryChanges = summaryChanges.applyTimelineEntries(
|
2021-03-03 11:27:18 +01:00
|
|
|
allEntries, isInitialSync, !this._isTimelineOpen, this._user.id);
|
2020-09-23 14:26:14 +02:00
|
|
|
// write summary changes, and unset if nothing was actually changed
|
|
|
|
summaryChanges = this._summary.writeData(summaryChanges, txn);
|
2021-03-02 21:29:32 +01:00
|
|
|
if (summaryChanges) {
|
|
|
|
log.set("summaryChanges", summaryChanges.diff(this._summary.data));
|
|
|
|
}
|
2020-08-21 18:11:07 +02:00
|
|
|
// fetch new members while we have txn open,
|
|
|
|
// but don't make any in-memory changes yet
|
|
|
|
let heroChanges;
|
2021-03-05 17:04:18 +01:00
|
|
|
// if any hero changes their display name, the summary in the room response
|
|
|
|
// is also updated, which will trigger a RoomSummary update
|
|
|
|
// and make summaryChanges non-falsy here
|
2020-09-23 14:26:14 +02:00
|
|
|
if (summaryChanges?.needsHeroes) {
|
2020-08-21 19:03:21 +02:00
|
|
|
// room name disappeared, open heroes
|
|
|
|
if (!this._heroes) {
|
|
|
|
this._heroes = new Heroes(this._roomId);
|
|
|
|
}
|
2020-08-31 09:50:57 +02:00
|
|
|
heroChanges = await this._heroes.calculateChanges(summaryChanges.heroes, memberChanges, txn);
|
2020-08-21 18:11:07 +02:00
|
|
|
}
|
2019-07-26 22:33:33 +02:00
|
|
|
let removedPendingEvents;
|
2020-09-23 17:34:25 +02:00
|
|
|
if (Array.isArray(roomResponse.timeline?.events)) {
|
2021-02-23 19:58:01 +01:00
|
|
|
removedPendingEvents = this._sendQueue.removeRemoteEchos(roomResponse.timeline.events, txn, log);
|
2019-07-26 22:33:33 +02:00
|
|
|
}
|
2020-08-21 18:11:07 +02:00
|
|
|
return {
|
|
|
|
summaryChanges,
|
2020-09-23 14:26:14 +02:00
|
|
|
roomEncryption,
|
2021-03-03 11:27:18 +01:00
|
|
|
newEntries,
|
|
|
|
updatedEntries: retryEntries || [],
|
2020-08-21 18:11:07 +02:00
|
|
|
newLiveKey,
|
|
|
|
removedPendingEvents,
|
2020-08-31 09:50:57 +02:00
|
|
|
memberChanges,
|
2020-09-08 14:38:27 +02:00
|
|
|
heroChanges,
|
2021-03-02 19:14:29 +01:00
|
|
|
shouldFlushKeyShares,
|
2020-08-21 18:11:07 +02:00
|
|
|
};
|
2019-02-27 19:27:45 +01:00
|
|
|
}
|
|
|
|
|
2020-09-08 14:39:33 +02:00
|
|
|
/**
|
|
|
|
* @package
|
|
|
|
* Called with the changes returned from `writeSync` to apply them and emit changes.
|
|
|
|
* No storage or network operations should be done here.
|
|
|
|
*/
|
2021-03-03 11:27:18 +01:00
|
|
|
afterSync(changes, log) {
|
|
|
|
const {
|
|
|
|
summaryChanges, newEntries, updatedEntries, newLiveKey,
|
|
|
|
removedPendingEvents, memberChanges,
|
|
|
|
heroChanges, roomEncryption
|
|
|
|
} = changes;
|
2021-02-17 18:45:04 +01:00
|
|
|
log.set("id", this.id);
|
2020-03-14 20:49:15 +01:00
|
|
|
this._syncWriter.afterSync(newLiveKey);
|
2020-09-23 14:26:14 +02:00
|
|
|
this._setEncryption(roomEncryption);
|
2020-08-31 09:50:57 +02:00
|
|
|
if (memberChanges.size) {
|
2020-08-19 16:12:49 +02:00
|
|
|
if (this._changedMembersDuringSync) {
|
2020-08-31 09:50:57 +02:00
|
|
|
for (const [userId, memberChange] of memberChanges.entries()) {
|
|
|
|
this._changedMembersDuringSync.set(userId, memberChange.member);
|
2020-08-19 16:12:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (this._memberList) {
|
2020-08-31 09:50:57 +02:00
|
|
|
this._memberList.afterSync(memberChanges);
|
2020-08-19 16:12:49 +02:00
|
|
|
}
|
2021-03-03 14:53:22 +01:00
|
|
|
if (this._timeline) {
|
|
|
|
for (const [userId, memberChange] of memberChanges.entries()) {
|
|
|
|
if (userId === this._user.id) {
|
|
|
|
this._timeline.updateOwnMember(memberChange.member);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-08-19 16:12:49 +02:00
|
|
|
}
|
2020-08-21 18:11:07 +02:00
|
|
|
let emitChange = false;
|
2020-03-14 20:46:49 +01:00
|
|
|
if (summaryChanges) {
|
2020-06-26 23:26:24 +02:00
|
|
|
this._summary.applyChanges(summaryChanges);
|
2020-09-23 14:26:14 +02:00
|
|
|
if (!this._summary.data.needsHeroes) {
|
2020-08-21 18:11:07 +02:00
|
|
|
this._heroes = null;
|
|
|
|
}
|
|
|
|
emitChange = true;
|
|
|
|
}
|
|
|
|
if (this._heroes && heroChanges) {
|
|
|
|
const oldName = this.name;
|
2020-09-23 14:26:14 +02:00
|
|
|
this._heroes.applyChanges(heroChanges, this._summary.data);
|
2020-08-21 18:11:07 +02:00
|
|
|
if (oldName !== this.name) {
|
|
|
|
emitChange = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (emitChange) {
|
2020-09-14 16:33:43 +02:00
|
|
|
this._emitUpdate();
|
2019-02-20 23:48:16 +01:00
|
|
|
}
|
2019-02-27 22:50:08 +01:00
|
|
|
if (this._timeline) {
|
2021-03-03 11:27:18 +01:00
|
|
|
// these should not be added if not already there
|
|
|
|
this._timeline.replaceEntries(updatedEntries);
|
|
|
|
this._timeline.addOrReplaceEntries(newEntries);
|
2019-02-27 22:50:08 +01:00
|
|
|
}
|
2020-10-30 15:19:51 +01:00
|
|
|
if (this._observedEvents) {
|
2021-03-03 11:27:18 +01:00
|
|
|
this._observedEvents.updateEvents(updatedEntries);
|
|
|
|
this._observedEvents.updateEvents(newEntries);
|
2020-10-30 15:19:51 +01:00
|
|
|
}
|
2019-07-26 22:33:33 +02:00
|
|
|
if (removedPendingEvents) {
|
|
|
|
this._sendQueue.emitRemovals(removedPendingEvents);
|
|
|
|
}
|
2020-09-23 18:22:51 +02:00
|
|
|
}
|
2018-12-21 14:35:24 +01:00
|
|
|
|
2021-03-02 19:14:29 +01:00
|
|
|
needsAfterSyncCompleted({shouldFlushKeyShares}) {
|
|
|
|
return shouldFlushKeyShares;
|
2020-09-10 12:11:43 +02:00
|
|
|
}
|
|
|
|
|
2020-09-08 14:38:27 +02:00
|
|
|
/**
|
|
|
|
* Only called if the result of writeSync had `needsAfterSyncCompleted` set.
|
|
|
|
* Can be used to do longer running operations that resulted from the last sync,
|
|
|
|
* like network operations.
|
|
|
|
*/
|
2021-04-20 17:39:46 +02:00
|
|
|
async afterSyncCompleted(changes, isNewRoom, log) {
|
2021-02-17 18:45:04 +01:00
|
|
|
log.set("id", this.id);
|
2020-09-08 14:38:27 +02:00
|
|
|
if (this._roomEncryption) {
|
2021-02-23 19:22:25 +01:00
|
|
|
await this._roomEncryption.flushPendingRoomKeyShares(this._hsApi, null, log);
|
2020-09-08 14:38:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @package */
|
2021-02-23 19:22:25 +01:00
|
|
|
start(pendingOperations, parentLog) {
|
2020-09-08 14:39:07 +02:00
|
|
|
if (this._roomEncryption) {
|
2021-02-23 19:22:25 +01:00
|
|
|
const roomKeyShares = pendingOperations?.get("share_room_key");
|
|
|
|
if (roomKeyShares) {
|
|
|
|
// if we got interrupted last time sending keys to newly joined members
|
|
|
|
parentLog.wrapDetached("flush room keys", log => {
|
|
|
|
log.set("id", this.id);
|
|
|
|
return this._roomEncryption.flushPendingRoomKeyShares(this._hsApi, roomKeyShares, log);
|
|
|
|
});
|
2020-09-08 14:39:07 +02:00
|
|
|
}
|
|
|
|
}
|
2021-02-23 19:22:25 +01:00
|
|
|
|
|
|
|
this._sendQueue.resumeSending(parentLog);
|
2019-07-26 22:40:39 +02:00
|
|
|
}
|
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @package */
|
2021-02-23 19:22:25 +01:00
|
|
|
async load(summary, txn, log) {
|
|
|
|
log.set("id", this.id);
|
2020-08-17 10:48:00 +02:00
|
|
|
try {
|
|
|
|
this._summary.load(summary);
|
2020-09-23 14:26:14 +02:00
|
|
|
if (this._summary.data.encryption) {
|
|
|
|
const roomEncryption = this._createRoomEncryption(this, this._summary.data.encryption);
|
|
|
|
this._setEncryption(roomEncryption);
|
2020-09-03 15:36:17 +02:00
|
|
|
}
|
2020-08-21 18:11:07 +02:00
|
|
|
// need to load members for name?
|
2020-09-23 14:26:14 +02:00
|
|
|
if (this._summary.data.needsHeroes) {
|
2020-08-21 18:11:07 +02:00
|
|
|
this._heroes = new Heroes(this._roomId);
|
2020-09-23 14:26:14 +02:00
|
|
|
const changes = await this._heroes.calculateChanges(this._summary.data.heroes, [], txn);
|
|
|
|
this._heroes.applyChanges(changes, this._summary.data);
|
2020-08-21 18:11:07 +02:00
|
|
|
}
|
2021-02-23 19:22:25 +01:00
|
|
|
return this._syncWriter.load(txn, log);
|
2020-08-17 10:48:00 +02:00
|
|
|
} catch (err) {
|
|
|
|
throw new WrappedError(`Could not load room ${this._roomId}`, err);
|
|
|
|
}
|
2020-09-23 18:22:51 +02:00
|
|
|
}
|
2019-02-26 22:45:58 +01:00
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @public */
|
2021-02-23 19:22:59 +01:00
|
|
|
sendEvent(eventType, content, attachments, log = null) {
|
|
|
|
this._platform.logger.wrapOrRun(log, "send", log => {
|
|
|
|
log.set("id", this.id);
|
|
|
|
return this._sendQueue.enqueueEvent(eventType, content, attachments, log);
|
|
|
|
});
|
2019-07-26 22:33:33 +02:00
|
|
|
}
|
|
|
|
|
2020-11-11 10:47:55 +01:00
|
|
|
/** @public */
|
2021-02-23 19:22:59 +01:00
|
|
|
async ensureMessageKeyIsShared(log = null) {
|
|
|
|
if (!this._roomEncryption) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return this._platform.logger.wrapOrRun(log, "ensureMessageKeyIsShared", log => {
|
|
|
|
log.set("id", this.id);
|
|
|
|
return this._roomEncryption.ensureMessageKeyIsShared(this._hsApi, log);
|
|
|
|
});
|
2020-11-06 23:43:02 +01:00
|
|
|
}
|
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @public */
|
2021-02-23 19:22:59 +01:00
|
|
|
async loadMemberList(log = null) {
|
2020-08-19 16:13:47 +02:00
|
|
|
if (this._memberList) {
|
2020-08-31 16:10:18 +02:00
|
|
|
// TODO: also await fetchOrLoadMembers promise here
|
2020-08-19 16:13:47 +02:00
|
|
|
this._memberList.retain();
|
|
|
|
return this._memberList;
|
|
|
|
} else {
|
2020-08-19 16:58:19 +02:00
|
|
|
const members = await fetchOrLoadMembers({
|
2020-08-19 16:44:09 +02:00
|
|
|
summary: this._summary,
|
|
|
|
roomId: this._roomId,
|
|
|
|
hsApi: this._hsApi,
|
|
|
|
storage: this._storage,
|
2020-09-09 09:50:03 +02:00
|
|
|
syncToken: this._getSyncToken(),
|
2020-08-19 16:44:09 +02:00
|
|
|
// to handle race between /members and /sync
|
|
|
|
setChangedMembersMap: map => this._changedMembersDuringSync = map,
|
2021-02-23 19:22:59 +01:00
|
|
|
log,
|
|
|
|
}, this._platform.logger);
|
2020-08-19 16:13:47 +02:00
|
|
|
this._memberList = new MemberList({
|
|
|
|
members,
|
|
|
|
closeCallback: () => { this._memberList = null; }
|
|
|
|
});
|
|
|
|
return this._memberList;
|
2020-06-26 23:26:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-21 23:40:40 +01:00
|
|
|
/** @public */
|
2021-02-23 19:58:01 +01:00
|
|
|
fillGap(fragmentEntry, amount, log = null) {
|
2020-08-19 16:13:47 +02:00
|
|
|
// TODO move some/all of this out of Room
|
2021-02-23 19:58:01 +01:00
|
|
|
return this._platform.logger.wrapOrRun(log, "fillGap", async log => {
|
2021-02-24 11:22:07 +01:00
|
|
|
log.set("id", this.id);
|
|
|
|
log.set("fragment", fragmentEntry.fragmentId);
|
|
|
|
log.set("dir", fragmentEntry.direction.asApiString());
|
2021-02-23 19:58:01 +01:00
|
|
|
if (fragmentEntry.edgeReached) {
|
2021-02-24 11:22:07 +01:00
|
|
|
log.set("edgeReached", true);
|
2021-02-23 19:58:01 +01:00
|
|
|
return;
|
2020-08-20 14:39:03 +02:00
|
|
|
}
|
2021-02-23 19:58:01 +01:00
|
|
|
const response = await this._hsApi.messages(this._roomId, {
|
|
|
|
from: fragmentEntry.token,
|
|
|
|
dir: fragmentEntry.direction.asApiString(),
|
|
|
|
limit: amount,
|
|
|
|
filter: {
|
|
|
|
lazy_load_members: true,
|
|
|
|
include_redundant_members: true,
|
|
|
|
}
|
|
|
|
}, {log}).response();
|
2020-03-22 00:07:37 +01:00
|
|
|
|
2021-03-04 19:47:02 +01:00
|
|
|
const txn = await this._storage.readWriteTxn([
|
2021-02-23 19:58:01 +01:00
|
|
|
this._storage.storeNames.pendingEvents,
|
|
|
|
this._storage.storeNames.timelineEvents,
|
|
|
|
this._storage.storeNames.timelineFragments,
|
|
|
|
]);
|
|
|
|
let removedPendingEvents;
|
|
|
|
let gapResult;
|
|
|
|
try {
|
|
|
|
// detect remote echos of pending messages in the gap
|
|
|
|
removedPendingEvents = this._sendQueue.removeRemoteEchos(response.chunk, txn, log);
|
|
|
|
// write new events into gap
|
|
|
|
const gapWriter = new GapWriter({
|
|
|
|
roomId: this._roomId,
|
|
|
|
storage: this._storage,
|
|
|
|
fragmentIdComparer: this._fragmentIdComparer,
|
|
|
|
});
|
2021-03-02 19:31:00 +01:00
|
|
|
gapResult = await gapWriter.writeFragmentFill(fragmentEntry, response, txn, log);
|
2021-02-23 19:58:01 +01:00
|
|
|
} catch (err) {
|
|
|
|
txn.abort();
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
await txn.complete();
|
|
|
|
if (this._roomEncryption) {
|
2021-03-15 15:23:35 +01:00
|
|
|
const decryptRequest = this._decryptEntries(DecryptionSource.Timeline, gapResult.entries, null, log);
|
2021-02-23 19:58:01 +01:00
|
|
|
await decryptRequest.complete();
|
|
|
|
}
|
|
|
|
// once txn is committed, update in-memory state & emit events
|
|
|
|
for (const fragment of gapResult.fragments) {
|
|
|
|
this._fragmentIdComparer.add(fragment);
|
|
|
|
}
|
|
|
|
if (removedPendingEvents) {
|
|
|
|
this._sendQueue.emitRemovals(removedPendingEvents);
|
|
|
|
}
|
|
|
|
if (this._timeline) {
|
2021-03-03 11:27:18 +01:00
|
|
|
this._timeline.addOrReplaceEntries(gapResult.entries);
|
2021-02-23 19:58:01 +01:00
|
|
|
}
|
|
|
|
});
|
2020-03-21 23:40:40 +01:00
|
|
|
}
|
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @public */
|
2019-02-26 22:45:58 +01:00
|
|
|
get name() {
|
2020-08-21 18:11:07 +02:00
|
|
|
if (this._heroes) {
|
|
|
|
return this._heroes.roomName;
|
|
|
|
}
|
2020-09-23 14:26:14 +02:00
|
|
|
const summaryData = this._summary.data;
|
|
|
|
if (summaryData.name) {
|
|
|
|
return summaryData.name;
|
|
|
|
}
|
|
|
|
if (summaryData.canonicalAlias) {
|
|
|
|
return summaryData.canonicalAlias;
|
|
|
|
}
|
|
|
|
return null;
|
2019-02-26 22:45:58 +01:00
|
|
|
}
|
2019-02-26 23:27:06 +01:00
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @public */
|
2019-02-26 23:27:06 +01:00
|
|
|
get id() {
|
|
|
|
return this._roomId;
|
|
|
|
}
|
2019-02-27 22:50:08 +01:00
|
|
|
|
2020-08-20 17:32:55 +02:00
|
|
|
get avatarUrl() {
|
2020-09-23 14:26:14 +02:00
|
|
|
if (this._summary.data.avatarUrl) {
|
|
|
|
return this._summary.data.avatarUrl;
|
2020-08-21 18:11:07 +02:00
|
|
|
} else if (this._heroes) {
|
|
|
|
return this._heroes.roomAvatarUrl;
|
|
|
|
}
|
|
|
|
return null;
|
2020-08-20 17:32:55 +02:00
|
|
|
}
|
|
|
|
|
2020-08-21 11:56:45 +02:00
|
|
|
get lastMessageTimestamp() {
|
2020-09-23 14:26:14 +02:00
|
|
|
return this._summary.data.lastMessageTimestamp;
|
2020-08-21 11:56:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
get isUnread() {
|
2020-09-23 14:26:14 +02:00
|
|
|
return this._summary.data.isUnread;
|
2020-08-21 11:56:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
get notificationCount() {
|
2020-09-23 14:26:14 +02:00
|
|
|
return this._summary.data.notificationCount;
|
2020-08-21 11:56:45 +02:00
|
|
|
}
|
2020-08-21 15:50:32 +02:00
|
|
|
|
|
|
|
get highlightCount() {
|
2020-09-23 14:26:14 +02:00
|
|
|
return this._summary.data.highlightCount;
|
2020-08-21 15:50:32 +02:00
|
|
|
}
|
2020-08-21 11:56:45 +02:00
|
|
|
|
2020-08-27 20:52:51 +02:00
|
|
|
get isLowPriority() {
|
2020-09-23 14:26:14 +02:00
|
|
|
const tags = this._summary.data.tags;
|
2020-08-27 20:52:51 +02:00
|
|
|
return !!(tags && tags['m.lowpriority']);
|
|
|
|
}
|
|
|
|
|
2020-08-28 14:36:00 +02:00
|
|
|
get isEncrypted() {
|
2020-09-23 14:26:14 +02:00
|
|
|
return !!this._summary.data.encryption;
|
2020-08-28 14:36:00 +02:00
|
|
|
}
|
|
|
|
|
2021-03-01 22:30:33 +01:00
|
|
|
get membership() {
|
|
|
|
return this._summary.data.membership;
|
|
|
|
}
|
|
|
|
|
2020-09-17 15:58:46 +02:00
|
|
|
enableSessionBackup(sessionBackup) {
|
|
|
|
this._roomEncryption?.enableSessionBackup(sessionBackup);
|
2021-03-02 21:30:00 +01:00
|
|
|
// TODO: do we really want to do this every time you open the app?
|
2021-03-02 15:29:35 +01:00
|
|
|
if (this._timeline) {
|
2021-03-15 15:24:57 +01:00
|
|
|
this._platform.logger.run("enableSessionBackup", log => {
|
|
|
|
return this._roomEncryption.restoreMissingSessionsFromBackup(this._timeline.remoteEntries, log);
|
|
|
|
});
|
2021-03-02 15:29:35 +01:00
|
|
|
}
|
2020-09-17 15:58:46 +02:00
|
|
|
}
|
|
|
|
|
2020-08-31 08:53:47 +02:00
|
|
|
get isTrackingMembers() {
|
2020-09-23 14:26:14 +02:00
|
|
|
return this._summary.data.isTrackingMembers;
|
2020-08-31 08:53:47 +02:00
|
|
|
}
|
|
|
|
|
2020-08-21 15:16:57 +02:00
|
|
|
async _getLastEventId() {
|
|
|
|
const lastKey = this._syncWriter.lastMessageKey;
|
|
|
|
if (lastKey) {
|
2021-03-04 19:47:02 +01:00
|
|
|
const txn = await this._storage.readTxn([
|
2020-08-21 15:16:57 +02:00
|
|
|
this._storage.storeNames.timelineEvents,
|
|
|
|
]);
|
|
|
|
const eventEntry = await txn.timelineEvents.get(this._roomId, lastKey);
|
|
|
|
return eventEntry?.event?.event_id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-10 12:09:17 +02:00
|
|
|
get _isTimelineOpen() {
|
|
|
|
return !!this._timeline;
|
|
|
|
}
|
|
|
|
|
2020-09-14 16:33:43 +02:00
|
|
|
_emitUpdate() {
|
|
|
|
// once for event emitter listeners
|
|
|
|
this.emit("change");
|
|
|
|
// and once for collection listeners
|
|
|
|
this._emitCollectionChange(this);
|
|
|
|
}
|
|
|
|
|
2021-02-24 11:22:19 +01:00
|
|
|
async clearUnread(log = null) {
|
2020-08-21 15:16:57 +02:00
|
|
|
if (this.isUnread || this.notificationCount) {
|
2021-02-24 11:22:19 +01:00
|
|
|
return await this._platform.logger.wrapOrRun(log, "clearUnread", async log => {
|
|
|
|
log.set("id", this.id);
|
2021-03-04 19:47:02 +01:00
|
|
|
const txn = await this._storage.readWriteTxn([
|
2021-02-24 11:22:19 +01:00
|
|
|
this._storage.storeNames.roomSummary,
|
|
|
|
]);
|
|
|
|
let data;
|
|
|
|
try {
|
|
|
|
data = this._summary.writeClearUnread(txn);
|
|
|
|
} catch (err) {
|
|
|
|
txn.abort();
|
2020-08-21 15:23:25 +02:00
|
|
|
throw err;
|
|
|
|
}
|
2021-02-24 11:22:19 +01:00
|
|
|
await txn.complete();
|
|
|
|
this._summary.applyChanges(data);
|
|
|
|
this._emitUpdate();
|
|
|
|
|
|
|
|
try {
|
|
|
|
const lastEventId = await this._getLastEventId();
|
|
|
|
if (lastEventId) {
|
|
|
|
await this._hsApi.receipt(this._roomId, "m.read", lastEventId);
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
// ignore ConnectionError
|
|
|
|
if (err.name !== "ConnectionError") {
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2020-08-21 11:56:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-19 16:58:28 +02:00
|
|
|
/** @public */
|
2021-02-24 11:21:04 +01:00
|
|
|
openTimeline(log = null) {
|
|
|
|
return this._platform.logger.wrapOrRun(log, "open timeline", async log => {
|
|
|
|
log.set("id", this.id);
|
|
|
|
if (this._timeline) {
|
|
|
|
throw new Error("not dealing with load race here for now");
|
|
|
|
}
|
|
|
|
this._timeline = new Timeline({
|
|
|
|
roomId: this.id,
|
|
|
|
storage: this._storage,
|
|
|
|
fragmentIdComparer: this._fragmentIdComparer,
|
|
|
|
pendingEvents: this._sendQueue.pendingEvents,
|
|
|
|
closeCallback: () => {
|
|
|
|
this._timeline = null;
|
|
|
|
if (this._roomEncryption) {
|
|
|
|
this._roomEncryption.notifyTimelineClosed();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
clock: this._platform.clock,
|
|
|
|
logger: this._platform.logger,
|
|
|
|
});
|
|
|
|
if (this._roomEncryption) {
|
|
|
|
this._timeline.enableEncryption(this._decryptEntries.bind(this, DecryptionSource.Timeline));
|
|
|
|
}
|
2021-04-08 16:30:46 +02:00
|
|
|
await this._timeline.load(this._user, this._summary.data.membership, log);
|
2021-02-24 11:21:04 +01:00
|
|
|
return this._timeline;
|
2019-02-27 22:50:08 +01:00
|
|
|
});
|
|
|
|
}
|
2020-05-09 20:02:08 +02:00
|
|
|
|
2020-08-20 15:40:43 +02:00
|
|
|
get mediaRepository() {
|
2020-09-22 13:40:38 +02:00
|
|
|
return this._mediaRepository;
|
2020-05-09 20:02:08 +02:00
|
|
|
}
|
2020-08-31 14:11:08 +02:00
|
|
|
|
|
|
|
/** @package */
|
|
|
|
writeIsTrackingMembers(value, txn) {
|
|
|
|
return this._summary.writeIsTrackingMembers(value, txn);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @package */
|
|
|
|
applyIsTrackingMembersChanges(changes) {
|
|
|
|
this._summary.applyChanges(changes);
|
|
|
|
}
|
2020-09-18 13:08:18 +02:00
|
|
|
|
2020-10-30 15:19:51 +01:00
|
|
|
observeEvent(eventId) {
|
|
|
|
if (!this._observedEvents) {
|
|
|
|
this._observedEvents = new ObservedEventMap(() => {
|
|
|
|
this._observedEvents = null;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
let entry = null;
|
|
|
|
if (this._timeline) {
|
|
|
|
entry = this._timeline.getByEventId(eventId);
|
|
|
|
}
|
|
|
|
const observable = this._observedEvents.observe(eventId, entry);
|
|
|
|
if (!entry) {
|
|
|
|
// update in the background
|
|
|
|
this._readEventById(eventId).then(entry => {
|
|
|
|
observable.update(entry);
|
|
|
|
}).catch(err => {
|
|
|
|
console.warn(`could not load event ${eventId} from storage`, err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return observable;
|
|
|
|
}
|
|
|
|
|
|
|
|
async _readEventById(eventId) {
|
|
|
|
let stores = [this._storage.storeNames.timelineEvents];
|
|
|
|
if (this.isEncrypted) {
|
|
|
|
stores.push(this._storage.storeNames.inboundGroupSessions);
|
|
|
|
}
|
2021-03-04 19:47:02 +01:00
|
|
|
const txn = await this._storage.readTxn(stores);
|
2020-10-30 15:19:51 +01:00
|
|
|
const storageEntry = await txn.timelineEvents.getByEventId(this._roomId, eventId);
|
|
|
|
if (storageEntry) {
|
|
|
|
const entry = new EventEntry(storageEntry, this._fragmentIdComparer);
|
|
|
|
if (entry.eventType === EVENT_ENCRYPTED_TYPE) {
|
|
|
|
const request = this._decryptEntries(DecryptionSource.Timeline, [entry], txn);
|
|
|
|
await request.complete();
|
|
|
|
}
|
|
|
|
return entry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-13 17:19:19 +01:00
|
|
|
createAttachment(blob, filename) {
|
2020-11-18 13:02:38 +01:00
|
|
|
return new AttachmentUpload({blob, filename, platform: this._platform});
|
2020-11-11 10:47:55 +01:00
|
|
|
}
|
|
|
|
|
2020-09-18 13:08:18 +02:00
|
|
|
dispose() {
|
|
|
|
this._roomEncryption?.dispose();
|
|
|
|
this._timeline?.dispose();
|
2020-11-18 20:08:42 +01:00
|
|
|
this._sendQueue.dispose();
|
2020-09-18 13:08:18 +02:00
|
|
|
}
|
2019-02-20 23:48:16 +01:00
|
|
|
}
|
2019-02-27 22:50:08 +01:00
|
|
|
|
2020-09-10 16:40:30 +02:00
|
|
|
class DecryptionRequest {
|
2021-03-15 15:23:46 +01:00
|
|
|
constructor(decryptFn, log) {
|
2020-09-10 16:40:30 +02:00
|
|
|
this._cancelled = false;
|
|
|
|
this.preparation = null;
|
2021-03-15 15:23:46 +01:00
|
|
|
this._promise = log.wrap("decryptEntries", log => decryptFn(this, log));
|
2020-09-10 16:40:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
complete() {
|
|
|
|
return this._promise;
|
|
|
|
}
|
|
|
|
|
|
|
|
get cancelled() {
|
|
|
|
return this._cancelled;
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
this._cancelled = true;
|
|
|
|
if (this.preparation) {
|
|
|
|
this.preparation.dispose();
|
|
|
|
}
|
|
|
|
}
|
2020-09-10 17:43:01 +02:00
|
|
|
}
|