From a4d924acd1864be651397cd7ff3ba000304ce435 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 20 Jan 2022 11:15:48 +0100 Subject: [PATCH 001/160] make KeyLoader use proper olm types --- .../e2ee/megolm/decryption/KeyLoader.ts | 45 +++++++------------ src/matrix/e2ee/megolm/decryption/RoomKey.ts | 10 +++-- .../megolm/decryption/SessionDecryption.ts | 4 +- 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/matrix/e2ee/megolm/decryption/KeyLoader.ts b/src/matrix/e2ee/megolm/decryption/KeyLoader.ts index 3aca957d..c1925e84 100644 --- a/src/matrix/e2ee/megolm/decryption/KeyLoader.ts +++ b/src/matrix/e2ee/megolm/decryption/KeyLoader.ts @@ -17,25 +17,14 @@ limitations under the License. import {isBetterThan, IncomingRoomKey} from "./RoomKey"; import {BaseLRUCache} from "../../../../utils/LRUCache"; import type {RoomKey} from "./RoomKey"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; export declare class OlmDecryptionResult { readonly plaintext: string; readonly message_index: number; } -export declare class OlmInboundGroupSession { - constructor(); - free(): void; - pickle(key: string | Uint8Array): string; - unpickle(key: string | Uint8Array, pickle: string); - create(session_key: string): string; - import_session(session_key: string): string; - decrypt(message: string): OlmDecryptionResult; - session_id(): string; - first_known_index(): number; - export_session(message_index: number): string; -} - /* Because Olm only has very limited memory available when compiled to wasm, we limit the amount of sessions held in memory. @@ -43,11 +32,11 @@ we limit the amount of sessions held in memory. export class KeyLoader extends BaseLRUCache { private pickleKey: string; - private olm: any; + private olm: Olm; private resolveUnusedOperation?: () => void; private operationBecomesUnusedPromise?: Promise; - constructor(olm: any, pickleKey: string, limit: number) { + constructor(olm: Olm, pickleKey: string, limit: number) { super(limit); this.pickleKey = pickleKey; this.olm = olm; @@ -60,7 +49,7 @@ export class KeyLoader extends BaseLRUCache { } } - async useKey(key: RoomKey, callback: (session: OlmInboundGroupSession, pickleKey: string) => Promise | T): Promise { + async useKey(key: RoomKey, callback: (session: Olm.InboundGroupSession, pickleKey: string) => Promise | T): Promise { const keyOp = await this.allocateOperation(key); try { return await callback(keyOp.session, this.pickleKey); @@ -186,11 +175,11 @@ export class KeyLoader extends BaseLRUCache { } class KeyOperation { - session: OlmInboundGroupSession; + session: Olm.InboundGroupSession; key: RoomKey; refCount: number; - constructor(key: RoomKey, session: OlmInboundGroupSession) { + constructor(key: RoomKey, session: Olm.InboundGroupSession) { this.key = key; this.session = session; this.refCount = 1; @@ -248,7 +237,7 @@ export function tests() { get serializationKey(): string { return `key-${this.sessionId}-${this._firstKnownIndex}`; } get serializationType(): string { return "type"; } get eventIds(): string[] | undefined { return undefined; } - loadInto(session: OlmInboundGroupSession) { + loadInto(session: Olm.InboundGroupSession) { const mockSession = session as MockInboundSession; mockSession.sessionId = this.sessionId; mockSession.firstKnownIndex = this._firstKnownIndex; @@ -284,7 +273,7 @@ export function tests() { return { "load key gives correct session": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); let callback1Called = false; let callback2Called = false; const p1 = loader.useKey(new MockRoomKey(roomId, aliceSenderKey, sessionId1, 1), async session => { @@ -305,7 +294,7 @@ export function tests() { assert(callback2Called); }, "keys with different first index are kept separate": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); let callback1Called = false; let callback2Called = false; const p1 = loader.useKey(new MockRoomKey(roomId, aliceSenderKey, sessionId1, 1), async session => { @@ -326,7 +315,7 @@ export function tests() { assert(callback2Called); }, "useKey blocks as long as no free sessions are available": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 1); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 1); let resolve; let callbackCalled = false; loader.useKey(new MockRoomKey(roomId, aliceSenderKey, sessionId1, 1), async session => { @@ -343,7 +332,7 @@ export function tests() { assert.equal(callbackCalled, true); }, "cache hit while key in use, then replace (check refCount works properly)": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 1); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 1); let resolve1, resolve2; const key1 = new MockRoomKey(roomId, aliceSenderKey, sessionId1, 1); const p1 = loader.useKey(key1, async session => { @@ -371,7 +360,7 @@ export function tests() { assert.equal(callbackCalled, true); }, "cache hit while key not in use": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); let resolve1, resolve2, invocations = 0; const key1 = new MockRoomKey(roomId, aliceSenderKey, sessionId1, 1); await loader.useKey(key1, async session => { invocations += 1; }); @@ -385,7 +374,7 @@ export function tests() { }, "dispose calls free on all sessions": async assert => { instances = 0; - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); await loader.useKey(new MockRoomKey(roomId, aliceSenderKey, sessionId1, 1), async session => {}); await loader.useKey(new MockRoomKey(roomId, aliceSenderKey, sessionId2, 1), async session => {}); assert.equal(instances, 2); @@ -395,7 +384,7 @@ export function tests() { assert.strictEqual(loader.size, 0, "loader.size"); }, "checkBetterThanKeyInStorage false with cache": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); const key1 = new MockRoomKey(roomId, aliceSenderKey, sessionId1, 2); await loader.useKey(key1, async session => {}); // fake we've checked with storage that this is the best key, @@ -409,7 +398,7 @@ export function tests() { assert.strictEqual(key2.isBetter, false); }, "checkBetterThanKeyInStorage true with cache": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); const key1 = new MockRoomKey(roomId, aliceSenderKey, sessionId1, 2); key1.isBetter = true; // fake we've check with storage so far (not including key2) this is the best key await loader.useKey(key1, async session => {}); @@ -420,7 +409,7 @@ export function tests() { assert.strictEqual(key2.isBetter, true); }, "prefer to remove worst key for a session from cache": async assert => { - const loader = new KeyLoader(olm, PICKLE_KEY, 2); + const loader = new KeyLoader(olm as any as Olm, PICKLE_KEY, 2); const key1 = new MockRoomKey(roomId, aliceSenderKey, sessionId1, 2); await loader.useKey(key1, async session => {}); key1.isBetter = true; // set to true just so it gets returned from getCachedKey diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index 81f1a9be..2cb65b33 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -17,7 +17,9 @@ limitations under the License. import type {InboundGroupSessionEntry} from "../../../storage/idb/stores/InboundGroupSessionStore"; import type {Transaction} from "../../../storage/idb/Transaction"; import type {DecryptionResult} from "../../DecryptionResult"; -import type {KeyLoader, OlmInboundGroupSession} from "./KeyLoader"; +import type {KeyLoader} from "./KeyLoader"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; export abstract class RoomKey { private _isBetter: boolean | undefined; @@ -33,7 +35,7 @@ export abstract class RoomKey { abstract get serializationKey(): string; abstract get serializationType(): string; abstract get eventIds(): string[] | undefined; - abstract loadInto(session: OlmInboundGroupSession, pickleKey: string): void; + abstract loadInto(session: Olm.InboundGroupSession, pickleKey: string): void; /* Whether the key has been checked against storage (or is from storage) * to be the better key for a given session. Given that all keys are checked to be better * as part of writing, we can trust that when this returns true, it really is the best key @@ -44,7 +46,7 @@ export abstract class RoomKey { set isBetter(value: boolean | undefined) { this._isBetter = value; } } -export function isBetterThan(newSession: OlmInboundGroupSession, existingSession: OlmInboundGroupSession) { +export function isBetterThan(newSession: Olm.InboundGroupSession, existingSession: Olm.InboundGroupSession) { return newSession.first_known_index() < existingSession.first_known_index(); } @@ -87,7 +89,7 @@ export abstract class IncomingRoomKey extends RoomKey { get eventIds() { return this._eventIds; } - private async _checkBetterThanKeyInStorage(loader: KeyLoader, callback: (((session: OlmInboundGroupSession, pickleKey: string) => void) | undefined), txn: Transaction): Promise { + private async _checkBetterThanKeyInStorage(loader: KeyLoader, callback: (((session: Olm.InboundGroupSession, pickleKey: string) => void) | undefined), txn: Transaction): Promise { if (this.isBetter !== undefined) { return this.isBetter; } diff --git a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts index 7e466806..f56feb47 100644 --- a/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts +++ b/src/matrix/e2ee/megolm/decryption/SessionDecryption.ts @@ -17,7 +17,7 @@ limitations under the License. import {DecryptionResult} from "../../DecryptionResult.js"; import {DecryptionError} from "../../common.js"; import {ReplayDetectionEntry} from "./ReplayDetectionEntry"; -import type {RoomKey} from "./RoomKey.js"; +import type {RoomKey} from "./RoomKey"; import type {KeyLoader, OlmDecryptionResult} from "./KeyLoader"; import type {OlmWorker} from "../../OlmWorker"; import type {TimelineEvent} from "../../../storage/types"; @@ -61,7 +61,7 @@ export class SessionDecryption { this.decryptionRequests!.push(request); decryptionResult = await request.response(); } else { - decryptionResult = session.decrypt(ciphertext); + decryptionResult = session.decrypt(ciphertext) as OlmDecryptionResult; } const {plaintext} = decryptionResult!; let payload; From 993a86ddb269f884ff1dcaf6492731a3d318cebb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 20 Jan 2022 11:16:08 +0100 Subject: [PATCH 002/160] convert SessionBackup to typescript and pass in keyloader --- src/matrix/Session.js | 17 ++-- src/matrix/e2ee/megolm/SessionBackup.js | 62 -------------- src/matrix/e2ee/megolm/SessionBackup.ts | 107 ++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 69 deletions(-) delete mode 100644 src/matrix/e2ee/megolm/SessionBackup.js create mode 100644 src/matrix/e2ee/megolm/SessionBackup.ts diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 8ccc71cc..0d7d057d 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -70,6 +70,7 @@ export class Session { this._e2eeAccount = null; this._deviceTracker = null; this._olmEncryption = null; + this._keyLoader = null; this._megolmEncryption = null; this._megolmDecryption = null; this._getSyncToken = () => this.syncToken; @@ -141,8 +142,8 @@ export class Session { now: this._platform.clock.now, ownDeviceId: this._sessionInfo.deviceId, }); - const keyLoader = new MegOlmKeyLoader(this._olm, PICKLE_KEY, 20); - this._megolmDecryption = new MegOlmDecryption(keyLoader, this._olmWorker); + this._keyLoader = new MegOlmKeyLoader(this._olm, PICKLE_KEY, 20); + this._megolmDecryption = new MegOlmDecryption(this._keyLoader, this._olmWorker); this._deviceMessageHandler.enableEncryption({olmDecryption, megolmDecryption: this._megolmDecryption}); } @@ -246,12 +247,14 @@ export class Session { async _createSessionBackup(ssssKey, txn) { const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); - this._sessionBackup = await SessionBackup.fromSecretStorage({ - platform: this._platform, - olm: this._olm, secretStorage, - hsApi: this._hsApi, + this._sessionBackup = await SessionBackup.fromSecretStorage( + this._platform, + this._olm, + secretStorage, + this._hsApi, + this._keyLoader, txn - }); + ); if (this._sessionBackup) { for (const room of this._rooms.values()) { if (room.isEncrypted) { diff --git a/src/matrix/e2ee/megolm/SessionBackup.js b/src/matrix/e2ee/megolm/SessionBackup.js deleted file mode 100644 index daba9961..00000000 --- a/src/matrix/e2ee/megolm/SessionBackup.js +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -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. -*/ - -export class SessionBackup { - constructor({backupInfo, decryption, hsApi}) { - this._backupInfo = backupInfo; - this._decryption = decryption; - this._hsApi = hsApi; - } - - async getSession(roomId, sessionId, log) { - const sessionResponse = await this._hsApi.roomKeyForRoomAndSession(this._backupInfo.version, roomId, sessionId, {log}).response(); - const sessionInfo = this._decryption.decrypt( - sessionResponse.session_data.ephemeral, - sessionResponse.session_data.mac, - sessionResponse.session_data.ciphertext, - ); - return JSON.parse(sessionInfo); - } - - get version() { - return this._backupInfo.version; - } - - dispose() { - this._decryption.free(); - } - - static async fromSecretStorage({platform, olm, secretStorage, hsApi, txn}) { - const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn); - if (base64PrivateKey) { - const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey)); - const backupInfo = await hsApi.roomKeysVersion().response(); - const expectedPubKey = backupInfo.auth_data.public_key; - const decryption = new olm.PkDecryption(); - try { - const pubKey = decryption.init_with_private_key(privateKey); - if (pubKey !== expectedPubKey) { - throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); - } - } catch(err) { - decryption.free(); - throw err; - } - return new SessionBackup({backupInfo, decryption, hsApi}); - } - } -} - diff --git a/src/matrix/e2ee/megolm/SessionBackup.ts b/src/matrix/e2ee/megolm/SessionBackup.ts new file mode 100644 index 00000000..9e875185 --- /dev/null +++ b/src/matrix/e2ee/megolm/SessionBackup.ts @@ -0,0 +1,107 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +import type {HomeServerApi} from "../../net/HomeServerApi"; +import type {RoomKey} from "./decryption/RoomKey"; +import type {KeyLoader} from "./decryption/KeyLoader"; +import type {SecretStorage} from "../../ssss/SecretStorage"; +import type {ILogItem} from "../../../logging/types"; +import type {Platform} from "../../../platform/web/Platform"; +import type {Transaction} from "../../storage/idb/Transaction"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; + +type SignatureMap = { + [userId: string]: {[deviceIdAndAlgorithm: string]: string} +} + +interface BaseBackupInfo { + version: string, + etag: string, + count: number, +} + +const Curve25519Algorithm = "m.megolm_backup.v1.curve25519-aes-sha2"; + +interface Curve25519BackupInfo extends BaseBackupInfo { + algorithm: typeof Curve25519Algorithm, + auth_data: Curve25519AuthData, +} + +interface OtherBackupInfo extends BaseBackupInfo { + algorithm: "other" +}; + +type BackupInfo = Curve25519BackupInfo | OtherBackupInfo; + +interface Curve25519AuthData { + public_key: string, + signatures: SignatureMap +} + +type AuthData = Curve25519AuthData; + +export class SessionBackup { + constructor( + private readonly backupInfo: BackupInfo, + private readonly decryption: Olm.PkDecryption, + private readonly hsApi: HomeServerApi, + private readonly keyLoader: KeyLoader + ) {} + + async getSession(roomId: string, sessionId: string, log: ILogItem) { + const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response(); + const sessionInfo = this.decryption.decrypt( + sessionResponse.session_data.ephemeral, + sessionResponse.session_data.mac, + sessionResponse.session_data.ciphertext, + ); + return JSON.parse(sessionInfo); + } + + get version() { + return this.backupInfo.version; + } + + dispose() { + this.decryption.free(); + } + + static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, txn: Transaction) { + const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn); + if (base64PrivateKey) { + const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey)); + const backupInfo = await hsApi.roomKeysVersion().response() as BackupInfo; + if (backupInfo.algorithm === Curve25519Algorithm) { + const expectedPubKey = backupInfo.auth_data.public_key; + const decryption = new olm.PkDecryption(); + try { + const pubKey = decryption.init_with_private_key(privateKey); + if (pubKey !== expectedPubKey) { + throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); + } + } catch(err) { + decryption.free(); + throw err; + } + return new SessionBackup(backupInfo, decryption, hsApi, keyLoader); + } else { + throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`); + } + } + } +} + From 5d87d8bde3710c6d05c5f9530f335559f3de51e9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:43:44 +0100 Subject: [PATCH 003/160] change store.get return type when no value is found to undefined IDBRequest.result is undefined according to the official TS type decls. --- src/matrix/storage/idb/QueryTarget.ts | 4 ++-- src/matrix/storage/idb/Store.ts | 2 +- src/matrix/storage/idb/stores/AccountDataStore.ts | 2 +- src/matrix/storage/idb/stores/DeviceIdentityStore.ts | 6 +++--- .../storage/idb/stores/GroupSessionDecryptionStore.ts | 2 +- src/matrix/storage/idb/stores/InboundGroupSessionStore.ts | 2 +- src/matrix/storage/idb/stores/OlmSessionStore.ts | 2 +- src/matrix/storage/idb/stores/OutboundGroupSessionStore.ts | 2 +- src/matrix/storage/idb/stores/RoomMemberStore.ts | 2 +- src/matrix/storage/idb/stores/RoomStateStore.ts | 2 +- src/matrix/storage/idb/stores/TimelineEventStore.ts | 4 ++-- src/matrix/storage/idb/stores/TimelineFragmentStore.ts | 2 +- src/matrix/storage/idb/stores/UserIdentityStore.ts | 2 +- 13 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/matrix/storage/idb/QueryTarget.ts b/src/matrix/storage/idb/QueryTarget.ts index 26821e0f..97b85569 100644 --- a/src/matrix/storage/idb/QueryTarget.ts +++ b/src/matrix/storage/idb/QueryTarget.ts @@ -37,7 +37,7 @@ interface QueryTargetInterface { openKeyCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest; supports(method: string): boolean; keyPath: string | string[]; - get(key: IDBValidKey | IDBKeyRange): IDBRequest; + get(key: IDBValidKey | IDBKeyRange): IDBRequest; getKey(key: IDBValidKey | IDBKeyRange): IDBRequest; } @@ -78,7 +78,7 @@ export class QueryTarget { return this._target.supports(methodName); } - get(key: IDBValidKey | IDBKeyRange): Promise { + get(key: IDBValidKey | IDBKeyRange): Promise { return reqAsPromise(this._target.get(key)); } diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts index 07cc90b0..f45b3f3e 100644 --- a/src/matrix/storage/idb/Store.ts +++ b/src/matrix/storage/idb/Store.ts @@ -91,7 +91,7 @@ export class QueryTargetWrapper { } } - get(key: IDBValidKey | IDBKeyRange): IDBRequest { + get(key: IDBValidKey | IDBKeyRange): IDBRequest { try { LOG_REQUESTS && logRequest("get", [key], this._qt); return this._qt.get(key); diff --git a/src/matrix/storage/idb/stores/AccountDataStore.ts b/src/matrix/storage/idb/stores/AccountDataStore.ts index 32aec513..2081ad8f 100644 --- a/src/matrix/storage/idb/stores/AccountDataStore.ts +++ b/src/matrix/storage/idb/stores/AccountDataStore.ts @@ -28,7 +28,7 @@ export class AccountDataStore { this._store = store; } - async get(type: string): Promise { + async get(type: string): Promise { return await this._store.get(type); } diff --git a/src/matrix/storage/idb/stores/DeviceIdentityStore.ts b/src/matrix/storage/idb/stores/DeviceIdentityStore.ts index 6b9f5332..2936f079 100644 --- a/src/matrix/storage/idb/stores/DeviceIdentityStore.ts +++ b/src/matrix/storage/idb/stores/DeviceIdentityStore.ts @@ -17,7 +17,7 @@ limitations under the License. import {MAX_UNICODE, MIN_UNICODE} from "./common"; import {Store} from "../Store"; -interface DeviceIdentity { +export interface DeviceIdentity { userId: string; deviceId: string; ed25519Key: string; @@ -65,7 +65,7 @@ export class DeviceIdentityStore { return deviceIds; } - get(userId: string, deviceId: string): Promise { + get(userId: string, deviceId: string): Promise { return this._store.get(encodeKey(userId, deviceId)); } @@ -74,7 +74,7 @@ export class DeviceIdentityStore { this._store.put(deviceIdentity); } - getByCurve25519Key(curve25519Key: string): Promise { + getByCurve25519Key(curve25519Key: string): Promise { return this._store.index("byCurve25519Key").get(curve25519Key); } diff --git a/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.ts b/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.ts index 1627e44a..a73c882d 100644 --- a/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.ts +++ b/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.ts @@ -35,7 +35,7 @@ export class GroupSessionDecryptionStore { this._store = store; } - get(roomId: string, sessionId: string, messageIndex: number): Promise { + get(roomId: string, sessionId: string, messageIndex: number): Promise { return this._store.get(encodeKey(roomId, sessionId, messageIndex)); } diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index 22093884..c670302c 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -46,7 +46,7 @@ export class InboundGroupSessionStore { return key === fetchedKey; } - get(roomId: string, senderKey: string, sessionId: string): Promise { + get(roomId: string, senderKey: string, sessionId: string): Promise { return this._store.get(encodeKey(roomId, senderKey, sessionId)); } diff --git a/src/matrix/storage/idb/stores/OlmSessionStore.ts b/src/matrix/storage/idb/stores/OlmSessionStore.ts index 3310215e..d5a79de2 100644 --- a/src/matrix/storage/idb/stores/OlmSessionStore.ts +++ b/src/matrix/storage/idb/stores/OlmSessionStore.ts @@ -62,7 +62,7 @@ export class OlmSessionStore { }); } - get(senderKey: string, sessionId: string): Promise { + get(senderKey: string, sessionId: string): Promise { return this._store.get(encodeKey(senderKey, sessionId)); } diff --git a/src/matrix/storage/idb/stores/OutboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/OutboundGroupSessionStore.ts index 9712c717..28ef23b0 100644 --- a/src/matrix/storage/idb/stores/OutboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/OutboundGroupSessionStore.ts @@ -32,7 +32,7 @@ export class OutboundGroupSessionStore { this._store.delete(roomId); } - get(roomId: string): Promise { + get(roomId: string): Promise { return this._store.get(roomId); } diff --git a/src/matrix/storage/idb/stores/RoomMemberStore.ts b/src/matrix/storage/idb/stores/RoomMemberStore.ts index 00cf607f..78024e7d 100644 --- a/src/matrix/storage/idb/stores/RoomMemberStore.ts +++ b/src/matrix/storage/idb/stores/RoomMemberStore.ts @@ -46,7 +46,7 @@ export class RoomMemberStore { this._roomMembersStore = roomMembersStore; } - get(roomId: string, userId: string): Promise { + get(roomId: string, userId: string): Promise { return this._roomMembersStore.get(encodeKey(roomId, userId)); } diff --git a/src/matrix/storage/idb/stores/RoomStateStore.ts b/src/matrix/storage/idb/stores/RoomStateStore.ts index b7ece0f7..d2bf811d 100644 --- a/src/matrix/storage/idb/stores/RoomStateStore.ts +++ b/src/matrix/storage/idb/stores/RoomStateStore.ts @@ -36,7 +36,7 @@ export class RoomStateStore { this._roomStateStore = idbStore; } - get(roomId: string, type: string, stateKey: string): Promise { + get(roomId: string, type: string, stateKey: string): Promise { const key = encodeKey(roomId, type, stateKey); return this._roomStateStore.get(key); } diff --git a/src/matrix/storage/idb/stores/TimelineEventStore.ts b/src/matrix/storage/idb/stores/TimelineEventStore.ts index bb6f652f..62a82fc0 100644 --- a/src/matrix/storage/idb/stores/TimelineEventStore.ts +++ b/src/matrix/storage/idb/stores/TimelineEventStore.ts @@ -301,11 +301,11 @@ export class TimelineEventStore { this._timelineStore.put(entry as TimelineEventStorageEntry); } - get(roomId: string, eventKey: EventKey): Promise { + get(roomId: string, eventKey: EventKey): Promise { return this._timelineStore.get(encodeKey(roomId, eventKey.fragmentId, eventKey.eventIndex)); } - getByEventId(roomId: string, eventId: string): Promise { + getByEventId(roomId: string, eventId: string): Promise { return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId)); } diff --git a/src/matrix/storage/idb/stores/TimelineFragmentStore.ts b/src/matrix/storage/idb/stores/TimelineFragmentStore.ts index 5753a93e..4e15589f 100644 --- a/src/matrix/storage/idb/stores/TimelineFragmentStore.ts +++ b/src/matrix/storage/idb/stores/TimelineFragmentStore.ts @@ -83,7 +83,7 @@ export class TimelineFragmentStore { this._store.put(fragment); } - get(roomId: string, fragmentId: number): Promise { + get(roomId: string, fragmentId: number): Promise { return this._store.get(encodeKey(roomId, fragmentId)); } diff --git a/src/matrix/storage/idb/stores/UserIdentityStore.ts b/src/matrix/storage/idb/stores/UserIdentityStore.ts index 692d8384..1c55baf0 100644 --- a/src/matrix/storage/idb/stores/UserIdentityStore.ts +++ b/src/matrix/storage/idb/stores/UserIdentityStore.ts @@ -28,7 +28,7 @@ export class UserIdentityStore { this._store = store; } - get(userId: string): Promise { + get(userId: string): Promise { return this._store.get(userId); } From a3e294bb6011ecad16f0466a5ba167893a0a1223 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:45:39 +0100 Subject: [PATCH 004/160] small cleanup --- src/matrix/e2ee/megolm/decryption/RoomKey.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index 2cb65b33..299b0a81 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -148,15 +148,8 @@ class DeviceMessageRoomKey extends IncomingRoomKey { } class BackupRoomKey extends IncomingRoomKey { - private _roomId: string; - private _sessionId: string; - private _backupInfo: string; - - constructor(roomId, sessionId, backupInfo) { + constructor(private _roomId: string, private _sessionId: string, private _backupInfo: object) { super(); - this._roomId = roomId; - this._sessionId = sessionId; - this._backupInfo = backupInfo; } get roomId() { return this._roomId; } From 290aaad63a9dbb7d3acd845f08edc9c2b5d55143 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:46:51 +0100 Subject: [PATCH 005/160] add sessionsNeedingBackup store --- src/matrix/storage/common.ts | 1 + src/matrix/storage/idb/Transaction.ts | 5 ++ src/matrix/storage/idb/schema.ts | 8 ++- .../idb/stores/SessionNeedingBackupStore.ts | 56 +++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts diff --git a/src/matrix/storage/common.ts b/src/matrix/storage/common.ts index 23bb0d31..3929c061 100644 --- a/src/matrix/storage/common.ts +++ b/src/matrix/storage/common.ts @@ -33,6 +33,7 @@ export enum StoreNames { groupSessionDecryptions = "groupSessionDecryptions", operations = "operations", accountData = "accountData", + sessionsNeedingBackup = "sessionsNeedingBackup", } export const STORE_NAMES: Readonly = Object.values(StoreNames); diff --git a/src/matrix/storage/idb/Transaction.ts b/src/matrix/storage/idb/Transaction.ts index 80894105..9b765310 100644 --- a/src/matrix/storage/idb/Transaction.ts +++ b/src/matrix/storage/idb/Transaction.ts @@ -36,6 +36,7 @@ import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore"; import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore"; import {OperationStore} from "./stores/OperationStore"; import {AccountDataStore} from "./stores/AccountDataStore"; +import {SessionNeedingBackupStore} from "./stores/SessionNeedingBackupStore"; import type {ILogger, ILogItem} from "../../../logging/types"; export type IDBKey = IDBValidKey | IDBKeyRange; @@ -151,6 +152,10 @@ export class Transaction { get inboundGroupSessions(): InboundGroupSessionStore { return this._store(StoreNames.inboundGroupSessions, idbStore => new InboundGroupSessionStore(idbStore)); } + + get sessionsNeedingBackup(): SessionNeedingBackupStore { + return this._store(StoreNames.sessionsNeedingBackup, idbStore => new SessionNeedingBackupStore(idbStore)); + } get outboundGroupSessions(): OutboundGroupSessionStore { return this._store(StoreNames.outboundGroupSessions, idbStore => new OutboundGroupSessionStore(idbStore)); diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index ad3e5896..44a04bba 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -31,7 +31,8 @@ export const schema: MigrationFunc[] = [ fixMissingRoomsInUserIdentities, changeSSSSKeyPrefix, backupAndRestoreE2EEAccountToLocalStorage, - clearAllStores + clearAllStores, + createSessionsNeedingBackup ]; // TODO: how to deal with git merge conflicts of this array? @@ -270,3 +271,8 @@ async function clearAllStores(db: IDBDatabase, txn: IDBTransaction) { } } } + +// v15 adds the sessionsNeedingBackup store, for session backup +function createSessionsNeedingBackup(db: IDBDatabase): void { + db.createObjectStore("sessionsNeedingBackup", {keyPath: "key"}); +} diff --git a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts new file mode 100644 index 00000000..27104191 --- /dev/null +++ b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts @@ -0,0 +1,56 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +import type {Store} from "../Store"; + +export type BackupEntry = { + roomId: string; + senderKey: string; + sessionId: string; +}; + +type StorageEntry = { + key: string +}; + +function encodeKey(roomId: string, senderKey: string, sessionId: string): string { + return `${roomId}|${senderKey}|${sessionId}`; +} + +function decodeKey(key: string): BackupEntry { + const [roomId, senderKey, sessionId] = key.split("|"); + return {roomId, senderKey, sessionId}; +} + +export class SessionNeedingBackupStore { + constructor(private store: Store) {} + + async getFirstEntries(amount: number): Promise { + const storageEntries = await this.store.selectLimit(undefined, amount); + return storageEntries.map(s => decodeKey(s.key)); + } + + set(roomId: string, senderKey: string, sessionId: string): void { + const storageEntry : StorageEntry = { + key: encodeKey(roomId, senderKey, sessionId), + }; + this.store.put(storageEntry); + } + + remove(roomId: string, senderKey: string, sessionId: string): void { + this.store.delete(encodeKey(roomId, senderKey, sessionId)); + } +} From 8f4e3c62cea231141ab8c6fe36277c221c622326 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:47:42 +0100 Subject: [PATCH 006/160] add hs endpoint for backup keys upload --- src/matrix/net/HomeServerApi.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index 3a6517cd..a23321f9 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -227,6 +227,10 @@ export class HomeServerApi { return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, undefined, options); } + uploadRoomKeysToBackup(version: string, payload: Record, options?: IRequestOptions): IHomeServerRequest { + return this._put(`/room_keys/keys`, {version}, payload, options); + } + uploadAttachment(blob: Blob, filename: string, options?: IRequestOptions): IHomeServerRequest { return this._authedRequest("POST", `${this._homeserver}/_matrix/media/r0/upload`, {filename}, blob, options); } From ffece4f3574bdea17f5b27acc9e5f95875daef01 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:48:03 +0100 Subject: [PATCH 007/160] move some validation of into session backup --- src/matrix/e2ee/RoomEncryption.js | 51 ++++++++++++++----------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index d9151a85..5a511d6e 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -182,35 +182,30 @@ export class RoomEncryption { log.set("id", sessionId); log.set("senderKey", senderKey); try { - const session = await this._sessionBackup.getSession(this._room.id, sessionId, log); - if (session?.algorithm === MEGOLM_ALGORITHM) { - let roomKey = this._megolmDecryption.roomKeyFromBackup(this._room.id, sessionId, session); - if (roomKey) { - if (roomKey.senderKey !== senderKey) { - log.set("wrong_sender_key", roomKey.senderKey); - log.logLevel = log.level.Warn; - return; - } - let keyIsBestOne = false; - let retryEventIds; - const txn = await this._storage.readWriteTxn([this._storage.storeNames.inboundGroupSessions]); - try { - keyIsBestOne = await this._megolmDecryption.writeRoomKey(roomKey, txn); - log.set("isBetter", keyIsBestOne); - if (keyIsBestOne) { - retryEventIds = roomKey.eventIds; - } - } catch (err) { - txn.abort(); - throw err; - } - await txn.complete(); - if (keyIsBestOne) { - await log.wrap("retryDecryption", log => this._room.notifyRoomKey(roomKey, retryEventIds || [], log)); - } + const roomKey = await this._sessionBackup.getRoomKey(this._room.id, sessionId, log); + if (roomKey) { + if (roomKey.senderKey !== senderKey) { + log.set("wrong_sender_key", roomKey.senderKey); + log.logLevel = log.level.Warn; + return; + } + let keyIsBestOne = false; + let retryEventIds; + const txn = await this._storage.readWriteTxn([this._storage.storeNames.inboundGroupSessions]); + try { + keyIsBestOne = await this._megolmDecryption.writeRoomKey(roomKey, txn); + log.set("isBetter", keyIsBestOne); + if (keyIsBestOne) { + retryEventIds = roomKey.eventIds; + } + } catch (err) { + txn.abort(); + throw err; + } + await txn.complete(); + if (keyIsBestOne) { + await log.wrap("retryDecryption", log => this._room.notifyRoomKey(roomKey, retryEventIds || [], log)); } - } else if (session?.algorithm) { - log.set("unknown algorithm", session.algorithm); } } catch (err) { if (!(err.name === "HomeServerError" && err.errcode === "M_NOT_FOUND")) { From 933a1b4636c0c05b3b2e38d9ca1a109eedb7dfd0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Jan 2022 18:48:19 +0100 Subject: [PATCH 008/160] draft of session backup writing + some refactoring --- src/matrix/Session.js | 9 ++ src/matrix/e2ee/megolm/SessionBackup.ts | 195 +++++++++++++++++++++--- 2 files changed, 179 insertions(+), 25 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 0d7d057d..94b68106 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -253,6 +253,7 @@ export class Session { secretStorage, this._hsApi, this._keyLoader, + this._storage, txn ); if (this._sessionBackup) { @@ -580,6 +581,11 @@ export class Session { if (preparation) { await log.wrap("deviceMsgs", log => this._deviceMessageHandler.writeSync(preparation, txn, log)); + // this should come after the deviceMessageHandler, so the room keys are already written and their + // isBetter property has been checked + if (this._sessionBackup) { + this._sessionBackup.writeKeys(preparation.newRoomKeys, txn, log); + } } // store account data @@ -617,6 +623,9 @@ export class Session { await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, false, log)); } } + if (this._sessionBackup) { + this._sessionBackup.flush(); + } } applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates) { diff --git a/src/matrix/e2ee/megolm/SessionBackup.ts b/src/matrix/e2ee/megolm/SessionBackup.ts index 9e875185..b76f3fca 100644 --- a/src/matrix/e2ee/megolm/SessionBackup.ts +++ b/src/matrix/e2ee/megolm/SessionBackup.ts @@ -14,10 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {StoreNames} from "../../storage/common"; +import {LRUCache} from "../../../utils/LRUCache"; +import {keyFromStorage, keyFromBackup} from "./decryption/RoomKey"; +import {MEGOLM_ALGORITHM} from "../common"; + import type {HomeServerApi} from "../../net/HomeServerApi"; -import type {RoomKey} from "./decryption/RoomKey"; +import type {IncomingRoomKey, RoomKey} from "./decryption/RoomKey"; import type {KeyLoader} from "./decryption/KeyLoader"; import type {SecretStorage} from "../../ssss/SecretStorage"; +import type {Storage} from "../../storage/idb/Storage"; +import type {DeviceIdentity} from "../../storage/idb/stores/DeviceIdentityStore"; import type {ILogItem} from "../../../logging/types"; import type {Platform} from "../../../platform/web/Platform"; import type {Transaction} from "../../storage/idb/Transaction"; @@ -47,6 +54,7 @@ interface OtherBackupInfo extends BaseBackupInfo { type BackupInfo = Curve25519BackupInfo | OtherBackupInfo; + interface Curve25519AuthData { public_key: string, signatures: SignatureMap @@ -54,50 +62,139 @@ interface Curve25519AuthData { type AuthData = Curve25519AuthData; +type SessionInfo = { + first_message_index: number, + forwarded_count: number, + is_verified: boolean, + session_data: Curve29915SessionData | any +} + +type Curve29915SessionData = { + ciphertext: string, + mac: string, + ephemeral: string, +} + +type MegOlmSessionKeyInfo = { + algorithm: MEGOLM_ALGORITHM, + sender_key: string, + sender_claimed_keys: {[algorithm: string]: string}, + forwarding_curve25519_key_chain: string[], + session_key: string +} + +type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; + export class SessionBackup { constructor( private readonly backupInfo: BackupInfo, - private readonly decryption: Olm.PkDecryption, + private readonly algorithm: Curve25519, private readonly hsApi: HomeServerApi, - private readonly keyLoader: KeyLoader + private readonly keyLoader: KeyLoader, + private readonly storage: Storage, + private readonly platform: Platform, ) {} - async getSession(roomId: string, sessionId: string, log: ILogItem) { + async getRoomKey(roomId: string, sessionId: string, log: ILogItem): Promise { const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response(); - const sessionInfo = this.decryption.decrypt( - sessionResponse.session_data.ephemeral, - sessionResponse.session_data.mac, - sessionResponse.session_data.ciphertext, - ); - return JSON.parse(sessionInfo); + if (!sessionResponse.session_data) { + return; + } + const sessionKeyInfo = this.algorithm.decryptRoomKey(sessionResponse.session_data); + if (sessionKeyInfo?.algorithm === MEGOLM_ALGORITHM) { + return keyFromBackup(roomId, sessionId, sessionKeyInfo); + } else if (sessionKeyInfo?.algorithm) { + log.set("unknown algorithm", sessionKeyInfo.algorithm); + } } - get version() { + writeKeys(roomKeys: IncomingRoomKey[], txn: Transaction): boolean { + let hasBetter = false; + for (const key of roomKeys) { + if (key.isBetter) { + txn.sessionsNeedingBackup.set(key.roomId, key.senderKey, key.sessionId); + hasBetter = true; + } + } + return hasBetter; + } + + async flush() { + while (true) { + await this.platform.clock.createTimeout(this.platform.random() * 10000).elapsed(); + const txn = await this.storage.readTxn([ + StoreNames.sessionsNeedingBackup, + StoreNames.inboundGroupSessions, + ]); + const keysNeedingBackup = await txn.sessionsNeedingBackup.getFirstEntries(20); + if (keysNeedingBackup.length === 0) { + return; + } + const roomKeys = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn))); + const payload: { + rooms: { + [roomId: string]: { + sessions: {[sessionId: string]: SessionInfo} + } + } + } = { rooms: {} }; + const payloadRooms = payload.rooms; + for (const key of roomKeys) { + if (key) { + let roomPayload = payloadRooms[key.roomId]; + if (!roomPayload) { + roomPayload = payloadRooms[key.roomId] = { sessions: {} }; + } + roomPayload.sessions[key.sessionId] = await this.encodeRoomKey(key); + } + } + await this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload).response(); + { + const txn = await this.storage.readWriteTxn([ + StoreNames.sessionsNeedingBackup, + ]); + try { + for (const key of keysNeedingBackup) { + txn.sessionsNeedingBackup.remove(key.roomId, key.senderKey, key.sessionId); + } + } catch (err) { + txn.abort(); + throw err; + } + await txn.complete(); + } + } + } + + private async encodeRoomKey(roomKey: RoomKey): Promise { + return await this.keyLoader.useKey(roomKey, session => { + const firstMessageIndex = session.first_known_index(); + const sessionKey = session.export_session(firstMessageIndex); + return { + first_message_index: firstMessageIndex, + forwarded_count: 0, + is_verified: false, + session_data: this.algorithm.encryptRoomKey(roomKey, sessionKey) + }; + }); + } + + get version(): string { return this.backupInfo.version; } dispose() { - this.decryption.free(); + this.algorithm.dispose(); } - static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, txn: Transaction) { + static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction) { const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn); if (base64PrivateKey) { const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey)); const backupInfo = await hsApi.roomKeysVersion().response() as BackupInfo; if (backupInfo.algorithm === Curve25519Algorithm) { - const expectedPubKey = backupInfo.auth_data.public_key; - const decryption = new olm.PkDecryption(); - try { - const pubKey = decryption.init_with_private_key(privateKey); - if (pubKey !== expectedPubKey) { - throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); - } - } catch(err) { - decryption.free(); - throw err; - } - return new SessionBackup(backupInfo, decryption, hsApi, keyLoader); + const algorithm = Curve25519.fromAuthData(backupInfo.auth_data, privateKey, olm); + return new SessionBackup(backupInfo, algorithm, hsApi, keyLoader, storage, platform); } else { throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`); } @@ -105,3 +202,51 @@ export class SessionBackup { } } +class Curve25519 { + constructor( + private readonly encryption: Olm.PkEncryption, + private readonly decryption: Olm.PkDecryption + ) {} + + static fromAuthData(authData: Curve25519AuthData, privateKey: Uint8Array, olm: Olm): Curve25519 { + const expectedPubKey = authData.public_key; + const decryption = new olm.PkDecryption(); + const encryption = new olm.PkEncryption(); + try { + const pubKey = decryption.init_with_private_key(privateKey); + if (pubKey !== expectedPubKey) { + throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); + } + encryption.set_recipient_key(pubKey); + } catch(err) { + decryption.free(); + throw err; + } + return new Curve25519(encryption, decryption); + } + + decryptRoomKey(sessionData: Curve29915SessionData): SessionKeyInfo { + const sessionInfo = this.decryption.decrypt( + sessionData.ephemeral, + sessionData.mac, + sessionData.ciphertext, + ); + return JSON.parse(sessionInfo) as SessionKeyInfo; + } + + encryptRoomKey(key: RoomKey, sessionKey: string): Curve29915SessionData { + const sessionInfo: SessionKeyInfo = { + algorithm: MEGOLM_ALGORITHM, + sender_key: key.senderKey, + sender_claimed_keys: {ed25519: key.claimedEd25519Key}, + forwarding_curve25519_key_chain: [], + session_key: sessionKey + }; + return this.encryption.encrypt(JSON.stringify(sessionInfo)) as Curve29915SessionData; + } + + dispose() { + this.decryption.free(); + this.encryption.free(); + } +} From 86caa5f9b1896489ad0a2bd6d5e09556f5c237b0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 09:51:48 +0100 Subject: [PATCH 009/160] rename session backup to key backup to be consistent with RoomKey --- src/domain/AccountSetupViewModel.js | 4 +- src/domain/session/SessionStatusViewModel.js | 14 ++--- ...ckupViewModel.js => KeyBackupViewModel.js} | 8 +-- .../session/settings/SettingsViewModel.js | 8 +-- src/matrix/Session.js | 58 +++++++++---------- src/matrix/e2ee/RoomEncryption.js | 16 ++--- .../KeyBackup.ts} | 32 +++++----- src/matrix/room/BaseRoom.js | 8 +-- src/matrix/storage/idb/schema.ts | 2 +- src/platform/web/ui/login/AccountSetupView.js | 4 +- .../web/ui/session/SessionStatusView.js | 2 +- ...ttingsView.js => KeyBackupSettingsView.js} | 2 +- .../web/ui/session/settings/SettingsView.js | 6 +- 13 files changed, 82 insertions(+), 82 deletions(-) rename src/domain/session/settings/{SessionBackupViewModel.js => KeyBackupViewModel.js} (94%) rename src/matrix/e2ee/megolm/{SessionBackup.ts => keybackup/KeyBackup.ts} (89%) rename src/platform/web/ui/session/settings/{SessionBackupSettingsView.js => KeyBackupSettingsView.js} (98%) diff --git a/src/domain/AccountSetupViewModel.js b/src/domain/AccountSetupViewModel.js index b2ce808a..4ad0d8d5 100644 --- a/src/domain/AccountSetupViewModel.js +++ b/src/domain/AccountSetupViewModel.js @@ -16,7 +16,7 @@ limitations under the License. import {ViewModel} from "./ViewModel.js"; import {KeyType} from "../matrix/ssss/index"; -import {Status} from "./session/settings/SessionBackupViewModel.js"; +import {Status} from "./session/settings/KeyBackupViewModel.js"; export class AccountSetupViewModel extends ViewModel { constructor(accountSetup) { @@ -50,7 +50,7 @@ export class AccountSetupViewModel extends ViewModel { } } -// this vm adopts the same shape as SessionBackupViewModel so the same view can be reused. +// this vm adopts the same shape as KeyBackupViewModel so the same view can be reused. class DecryptDehydratedDeviceViewModel extends ViewModel { constructor(accountSetupViewModel, decryptedCallback) { super(); diff --git a/src/domain/session/SessionStatusViewModel.js b/src/domain/session/SessionStatusViewModel.js index fcedb371..3f2263ac 100644 --- a/src/domain/session/SessionStatusViewModel.js +++ b/src/domain/session/SessionStatusViewModel.js @@ -36,7 +36,7 @@ export class SessionStatusViewModel extends ViewModel { this._reconnector = reconnector; this._status = this._calculateState(reconnector.connectionStatus.get(), sync.status.get()); this._session = session; - this._setupSessionBackupUrl = this.urlCreator.urlForSegment("settings"); + this._setupKeyBackupUrl = this.urlCreator.urlForSegment("settings"); this._dismissSecretStorage = false; } @@ -44,17 +44,17 @@ export class SessionStatusViewModel extends ViewModel { const update = () => this._updateStatus(); this.track(this._sync.status.subscribe(update)); this.track(this._reconnector.connectionStatus.subscribe(update)); - this.track(this._session.needsSessionBackup.subscribe(() => { + this.track(this._session.needsKeyBackup.subscribe(() => { this.emitChange(); })); } - get setupSessionBackupUrl () { - return this._setupSessionBackupUrl; + get setupKeyBackupUrl () { + return this._setupKeyBackupUrl; } get isShown() { - return (this._session.needsSessionBackup.get() && !this._dismissSecretStorage) || this._status !== SessionStatus.Syncing; + return (this._session.needsKeyBackup.get() && !this._dismissSecretStorage) || this._status !== SessionStatus.Syncing; } get statusLabel() { @@ -70,7 +70,7 @@ export class SessionStatusViewModel extends ViewModel { case SessionStatus.SyncError: return this.i18n`Sync failed because of ${this._sync.error}`; } - if (this._session.needsSessionBackup.get()) { + if (this._session.needsKeyBackup.get()) { return this.i18n`Set up session backup to decrypt older messages.`; } return ""; @@ -135,7 +135,7 @@ export class SessionStatusViewModel extends ViewModel { get isSecretStorageShown() { // TODO: we need a model here where we can have multiple messages queued up and their buttons don't bleed into each other. - return this._status === SessionStatus.Syncing && this._session.needsSessionBackup.get() && !this._dismissSecretStorage; + return this._status === SessionStatus.Syncing && this._session.needsKeyBackup.get() && !this._dismissSecretStorage; } get canDismiss() { diff --git a/src/domain/session/settings/SessionBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js similarity index 94% rename from src/domain/session/settings/SessionBackupViewModel.js rename to src/domain/session/settings/KeyBackupViewModel.js index 5a127904..38d37c18 100644 --- a/src/domain/session/settings/SessionBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -20,7 +20,7 @@ import {createEnum} from "../../../utils/enum"; export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending"); -export class SessionBackupViewModel extends ViewModel { +export class KeyBackupViewModel extends ViewModel { constructor(options) { super(options); this._session = options.session; @@ -43,7 +43,7 @@ export class SessionBackupViewModel extends ViewModel { let status; const hasSecretStorageKey = this._session.hasSecretStorageKey.get(); if (hasSecretStorageKey === true) { - status = this._session.sessionBackup ? Status.Enabled : Status.SetupKey; + status = this._session.keyBackup ? Status.Enabled : Status.SetupKey; } else if (hasSecretStorageKey === false) { status = Status.SetupKey; } else { @@ -59,7 +59,7 @@ export class SessionBackupViewModel extends ViewModel { } get purpose() { - return this.i18n`set up session backup`; + return this.i18n`set up key backup`; } offerDehydratedDeviceSetup() { @@ -75,7 +75,7 @@ export class SessionBackupViewModel extends ViewModel { } get backupVersion() { - return this._session.sessionBackup?.version; + return this._session.keyBackup?.version; } get status() { diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index 70e507b8..0b68f168 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -15,7 +15,7 @@ limitations under the License. */ import {ViewModel} from "../../ViewModel.js"; -import {SessionBackupViewModel} from "./SessionBackupViewModel.js"; +import {KeyBackupViewModel} from "./KeyBackupViewModel.js"; class PushNotificationStatus { constructor() { @@ -43,7 +43,7 @@ export class SettingsViewModel extends ViewModel { this._updateService = options.updateService; const {client} = options; this._client = client; - this._sessionBackupViewModel = this.track(new SessionBackupViewModel(this.childOptions({session: this._session}))); + this._keyBackupViewModel = this.track(new KeyBackupViewModel(this.childOptions({session: this._session}))); this._closeUrl = this.urlCreator.urlUntilSegment("session"); this._estimate = null; this.sentImageSizeLimit = null; @@ -115,8 +115,8 @@ export class SettingsViewModel extends ViewModel { return !!this.platform.updateService; } - get sessionBackupViewModel() { - return this._sessionBackupViewModel; + get keyBackupViewModel() { + return this._keyBackupViewModel; } get storageQuota() { diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 94b68106..9510cbfd 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -29,7 +29,7 @@ import {Decryption as OlmDecryption} from "./e2ee/olm/Decryption.js"; import {Encryption as OlmEncryption} from "./e2ee/olm/Encryption.js"; import {Decryption as MegOlmDecryption} from "./e2ee/megolm/Decryption"; import {KeyLoader as MegOlmKeyLoader} from "./e2ee/megolm/decryption/KeyLoader"; -import {SessionBackup} from "./e2ee/megolm/SessionBackup.js"; +import {KeyBackup} from "./e2ee/megolm/keybackup/KeyBackup"; import {Encryption as MegOlmEncryption} from "./e2ee/megolm/Encryption.js"; import {MEGOLM_ALGORITHM} from "./e2ee/common.js"; import {RoomEncryption} from "./e2ee/RoomEncryption.js"; @@ -75,7 +75,7 @@ export class Session { this._megolmDecryption = null; this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; - this._sessionBackup = null; + this._keyBackup = null; this._hasSecretStorageKey = new ObservableValue(null); this._observedRoomStatus = new Map(); @@ -91,7 +91,7 @@ export class Session { } this._createRoomEncryption = this._createRoomEncryption.bind(this); this._forgetArchivedRoom = this._forgetArchivedRoom.bind(this); - this.needsSessionBackup = new ObservableValue(false); + this.needsKeyBackup = new ObservableValue(false); } get fingerprintKey() { @@ -170,11 +170,11 @@ export class Session { megolmEncryption: this._megolmEncryption, megolmDecryption: this._megolmDecryption, storage: this._storage, - sessionBackup: this._sessionBackup, + keyBackup: this._keyBackup, encryptionParams, notifyMissingMegolmSession: () => { - if (!this._sessionBackup) { - this.needsSessionBackup.set(true) + if (!this._keyBackup) { + this.needsKeyBackup.set(true) } }, clock: this._platform.clock @@ -183,7 +183,7 @@ export class Session { /** * Enable secret storage by providing the secret storage credential. - * This will also see if there is a megolm session backup and try to enable that if so. + * This will also see if there is a megolm key backup and try to enable that if so. * * @param {string} type either "passphrase" or "recoverykey" * @param {string} credential either the passphrase or the recovery key, depending on the type @@ -193,15 +193,15 @@ export class Session { if (!this._olm) { throw new Error("olm required"); } - if (this._sessionBackup) { + if (this._keyBackup) { return false; } const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); - // and create session backup, which needs to read from accountData + // and create key backup, which needs to read from accountData const readTxn = await this._storage.readTxn([ this._storage.storeNames.accountData, ]); - await this._createSessionBackup(key, readTxn); + await this._createKeyBackup(key, readTxn); await this._writeSSSSKey(key); this._hasSecretStorageKey.set(true); return key; @@ -233,21 +233,21 @@ export class Session { throw err; } await writeTxn.complete(); - if (this._sessionBackup) { + if (this._keyBackup) { for (const room of this._rooms.values()) { if (room.isEncrypted) { - room.enableSessionBackup(undefined); + room.enableKeyBackup(undefined); } } - this._sessionBackup?.dispose(); - this._sessionBackup = undefined; + this._keyBackup?.dispose(); + this._keyBackup = undefined; } this._hasSecretStorageKey.set(false); } - async _createSessionBackup(ssssKey, txn) { + async _createKeyBackup(ssssKey, txn) { const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); - this._sessionBackup = await SessionBackup.fromSecretStorage( + this._keyBackup = await KeyBackup.fromSecretStorage( this._platform, this._olm, secretStorage, @@ -256,18 +256,18 @@ export class Session { this._storage, txn ); - if (this._sessionBackup) { + if (this._keyBackup) { for (const room of this._rooms.values()) { if (room.isEncrypted) { - room.enableSessionBackup(this._sessionBackup); + room.enableKeyBackup(this._keyBackup); } } } - this.needsSessionBackup.set(false); + this.needsKeyBackup.set(false); } - get sessionBackup() { - return this._sessionBackup; + get keyBackup() { + return this._keyBackup; } get hasIdentity() { @@ -405,8 +405,8 @@ export class Session { dispose() { this._olmWorker?.dispose(); this._olmWorker = undefined; - this._sessionBackup?.dispose(); - this._sessionBackup = undefined; + this._keyBackup?.dispose(); + this._keyBackup = undefined; this._megolmDecryption?.dispose(); this._megolmDecryption = undefined; this._e2eeAccount?.dispose(); @@ -434,7 +434,7 @@ export class Session { await txn.complete(); } // enable session backup, this requests the latest backup version - if (!this._sessionBackup) { + if (!this._keyBackup) { if (dehydratedDevice) { await log.wrap("SSSSKeyFromDehydratedDeviceKey", async log => { const ssssKey = await createSSSSKeyFromDehydratedDeviceKey(dehydratedDevice.key, this._storage, this._platform); @@ -452,7 +452,7 @@ export class Session { const ssssKey = await ssssReadKey(txn); if (ssssKey) { // txn will end here as this does a network request - await this._createSessionBackup(ssssKey, txn); + await this._createKeyBackup(ssssKey, txn); } this._hasSecretStorageKey.set(!!ssssKey); } @@ -583,8 +583,8 @@ export class Session { await log.wrap("deviceMsgs", log => this._deviceMessageHandler.writeSync(preparation, txn, log)); // this should come after the deviceMessageHandler, so the room keys are already written and their // isBetter property has been checked - if (this._sessionBackup) { - this._sessionBackup.writeKeys(preparation.newRoomKeys, txn, log); + if (this._keyBackup) { + this._keyBackup.writeKeys(preparation.newRoomKeys, txn, log); } } @@ -623,8 +623,8 @@ export class Session { await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, false, log)); } } - if (this._sessionBackup) { - this._sessionBackup.flush(); + if (this._keyBackup) { + this._keyBackup.flush(); } } diff --git a/src/matrix/e2ee/RoomEncryption.js b/src/matrix/e2ee/RoomEncryption.js index 5a511d6e..2e6ee254 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -28,7 +28,7 @@ const MIN_PRESHARE_INTERVAL = 60 * 1000; // 1min // TODO: this class is a good candidate for splitting up into encryption and decryption, there doesn't seem to be much overlap export class RoomEncryption { - constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, sessionBackup, notifyMissingMegolmSession, clock}) { + constructor({room, deviceTracker, olmEncryption, megolmEncryption, megolmDecryption, encryptionParams, storage, keyBackup, notifyMissingMegolmSession, clock}) { this._room = room; this._deviceTracker = deviceTracker; this._olmEncryption = olmEncryption; @@ -39,7 +39,7 @@ export class RoomEncryption { // caches devices to verify events this._senderDeviceCache = new Map(); this._storage = storage; - this._sessionBackup = sessionBackup; + this._keyBackup = keyBackup; this._notifyMissingMegolmSession = notifyMissingMegolmSession; this._clock = clock; this._isFlushingRoomKeyShares = false; @@ -48,11 +48,11 @@ export class RoomEncryption { this._disposed = false; } - enableSessionBackup(sessionBackup) { - if (this._sessionBackup && !!sessionBackup) { + enableKeyBackup(keyBackup) { + if (this._keyBackup && !!keyBackup) { return; } - this._sessionBackup = sessionBackup; + this._keyBackup = keyBackup; } async restoreMissingSessionsFromBackup(entries, log) { @@ -130,7 +130,7 @@ export class RoomEncryption { })); } - if (!this._sessionBackup) { + if (!this._keyBackup) { return; } @@ -174,7 +174,7 @@ export class RoomEncryption { async _requestMissingSessionFromBackup(senderKey, sessionId, log) { // show prompt to enable secret storage - if (!this._sessionBackup) { + if (!this._keyBackup) { log.set("enabled", false); this._notifyMissingMegolmSession(); return; @@ -182,7 +182,7 @@ export class RoomEncryption { log.set("id", sessionId); log.set("senderKey", senderKey); try { - const roomKey = await this._sessionBackup.getRoomKey(this._room.id, sessionId, log); + const roomKey = await this._keyBackup.getRoomKey(this._room.id, sessionId, log); if (roomKey) { if (roomKey.senderKey !== senderKey) { log.set("wrong_sender_key", roomKey.senderKey); diff --git a/src/matrix/e2ee/megolm/SessionBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts similarity index 89% rename from src/matrix/e2ee/megolm/SessionBackup.ts rename to src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index b76f3fca..98456973 100644 --- a/src/matrix/e2ee/megolm/SessionBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -14,20 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {StoreNames} from "../../storage/common"; -import {LRUCache} from "../../../utils/LRUCache"; -import {keyFromStorage, keyFromBackup} from "./decryption/RoomKey"; -import {MEGOLM_ALGORITHM} from "../common"; +import {StoreNames} from "../../../storage/common"; +import {LRUCache} from "../../../../utils/LRUCache"; +import {keyFromStorage, keyFromBackup} from "../decryption/RoomKey"; +import {MEGOLM_ALGORITHM} from "../../common"; -import type {HomeServerApi} from "../../net/HomeServerApi"; -import type {IncomingRoomKey, RoomKey} from "./decryption/RoomKey"; -import type {KeyLoader} from "./decryption/KeyLoader"; -import type {SecretStorage} from "../../ssss/SecretStorage"; -import type {Storage} from "../../storage/idb/Storage"; -import type {DeviceIdentity} from "../../storage/idb/stores/DeviceIdentityStore"; -import type {ILogItem} from "../../../logging/types"; -import type {Platform} from "../../../platform/web/Platform"; -import type {Transaction} from "../../storage/idb/Transaction"; +import type {HomeServerApi} from "../../../net/HomeServerApi"; +import type {IncomingRoomKey, RoomKey} from "../decryption/RoomKey"; +import type {KeyLoader} from "../decryption/KeyLoader"; +import type {SecretStorage} from "../../../ssss/SecretStorage"; +import type {Storage} from "../../../storage/idb/Storage"; +import type {DeviceIdentity} from "../../../storage/idb/stores/DeviceIdentityStore"; +import type {ILogItem} from "../../../../logging/types"; +import type {Platform} from "../../../../platform/web/Platform"; +import type {Transaction} from "../../../storage/idb/Transaction"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; @@ -85,7 +85,7 @@ type MegOlmSessionKeyInfo = { type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; -export class SessionBackup { +export class KeyBackup { constructor( private readonly backupInfo: BackupInfo, private readonly algorithm: Curve25519, @@ -187,14 +187,14 @@ export class SessionBackup { this.algorithm.dispose(); } - static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction) { + static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction): Promise { const base64PrivateKey = await secretStorage.readSecret("m.megolm_backup.v1", txn); if (base64PrivateKey) { const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey)); const backupInfo = await hsApi.roomKeysVersion().response() as BackupInfo; if (backupInfo.algorithm === Curve25519Algorithm) { const algorithm = Curve25519.fromAuthData(backupInfo.auth_data, privateKey, olm); - return new SessionBackup(backupInfo, algorithm, hsApi, keyLoader, storage, platform); + return new KeyBackup(backupInfo, algorithm, hsApi, keyLoader, storage, platform); } else { throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`); } diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 39232ae5..ecd2d860 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -461,11 +461,11 @@ export class BaseRoom extends EventEmitter { return observable; } - enableSessionBackup(sessionBackup) { - this._roomEncryption?.enableSessionBackup(sessionBackup); + enableKeyBackup(keyBackup) { + this._roomEncryption?.enableKeyBackup(keyBackup); // TODO: do we really want to do this every time you open the app? - if (this._timeline && sessionBackup) { - this._platform.logger.run("enableSessionBackup", log => { + if (this._timeline && keyBackup) { + this._platform.logger.run("enableKeyBackup", log => { return this._roomEncryption.restoreMissingSessionsFromBackup(this._timeline.remoteEntries, log); }); } diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 44a04bba..2aca60a2 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -272,7 +272,7 @@ async function clearAllStores(db: IDBDatabase, txn: IDBTransaction) { } } -// v15 adds the sessionsNeedingBackup store, for session backup +// v15 adds the sessionsNeedingBackup store, for key backup function createSessionsNeedingBackup(db: IDBDatabase): void { db.createObjectStore("sessionsNeedingBackup", {keyPath: "key"}); } diff --git a/src/platform/web/ui/login/AccountSetupView.js b/src/platform/web/ui/login/AccountSetupView.js index 32a4afb5..e0d41693 100644 --- a/src/platform/web/ui/login/AccountSetupView.js +++ b/src/platform/web/ui/login/AccountSetupView.js @@ -15,13 +15,13 @@ limitations under the License. */ import {TemplateView} from "../general/TemplateView"; -import {SessionBackupSettingsView} from "../session/settings/SessionBackupSettingsView.js"; +import {KeyBackupSettingsView} from "../session/settings/KeyBackupSettingsView.js"; export class AccountSetupView extends TemplateView { render(t, vm) { return t.div({className: "Settings" /* hack for now to get the layout right*/}, [ t.h3(vm.i18n`Restore your encrypted history?`), - t.ifView(vm => vm.decryptDehydratedDeviceViewModel, vm => new SessionBackupSettingsView(vm.decryptDehydratedDeviceViewModel)), + t.ifView(vm => vm.decryptDehydratedDeviceViewModel, vm => new KeyBackupSettingsView(vm.decryptDehydratedDeviceViewModel)), t.map(vm => vm.deviceDecrypted, (decrypted, t) => { if (decrypted) { return t.p(vm.i18n`That worked out, you're good to go!`); diff --git a/src/platform/web/ui/session/SessionStatusView.js b/src/platform/web/ui/session/SessionStatusView.js index bd8c6dbb..a629b2a1 100644 --- a/src/platform/web/ui/session/SessionStatusView.js +++ b/src/platform/web/ui/session/SessionStatusView.js @@ -26,7 +26,7 @@ export class SessionStatusView extends TemplateView { spinner(t, {hidden: vm => !vm.isWaiting}), t.p(vm => vm.statusLabel), t.if(vm => vm.isConnectNowShown, t => t.button({className: "link", onClick: () => vm.connectNow()}, "Retry now")), - t.if(vm => vm.isSecretStorageShown, t => t.a({href: vm.setupSessionBackupUrl}, "Go to settings")), + t.if(vm => vm.isSecretStorageShown, t => t.a({href: vm.setupKeyBackupUrl}, "Go to settings")), t.if(vm => vm.canDismiss, t => t.div({className: "end"}, t.button({className: "dismiss", onClick: () => vm.dismiss()}))), ]); } diff --git a/src/platform/web/ui/session/settings/SessionBackupSettingsView.js b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js similarity index 98% rename from src/platform/web/ui/session/settings/SessionBackupSettingsView.js rename to src/platform/web/ui/session/settings/KeyBackupSettingsView.js index b8206c55..bf3fd296 100644 --- a/src/platform/web/ui/session/settings/SessionBackupSettingsView.js +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js @@ -17,7 +17,7 @@ limitations under the License. import {TemplateView, InlineTemplateView} from "../../general/TemplateView"; import {StaticView} from "../../general/StaticView.js"; -export class SessionBackupSettingsView extends TemplateView { +export class KeyBackupSettingsView extends TemplateView { render(t, vm) { return t.mapView(vm => vm.status, status => { switch (status) { diff --git a/src/platform/web/ui/session/settings/SettingsView.js b/src/platform/web/ui/session/settings/SettingsView.js index 78ca2007..93e44307 100644 --- a/src/platform/web/ui/session/settings/SettingsView.js +++ b/src/platform/web/ui/session/settings/SettingsView.js @@ -15,7 +15,7 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView"; -import {SessionBackupSettingsView} from "./SessionBackupSettingsView.js" +import {KeyBackupSettingsView} from "./KeyBackupSettingsView.js" export class SettingsView extends TemplateView { render(t, vm) { @@ -47,8 +47,8 @@ export class SettingsView extends TemplateView { }, vm.i18n`Log out`)), ); settingNodes.push( - t.h3("Session Backup"), - t.view(new SessionBackupSettingsView(vm.sessionBackupViewModel)) + t.h3("Key backup"), + t.view(new KeyBackupSettingsView(vm.keyBackupViewModel)) ); settingNodes.push( From cfb94206f90ea66f71e9ddaa52c805b67cc9cbc1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 10:13:01 +0100 Subject: [PATCH 010/160] move curve25519 code to separate file --- .../e2ee/megolm/keybackup/Curve25519.ts | 89 ++++++++++++++ src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 115 ++++-------------- 2 files changed, 113 insertions(+), 91 deletions(-) create mode 100644 src/matrix/e2ee/megolm/keybackup/Curve25519.ts diff --git a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts new file mode 100644 index 00000000..9a453b5a --- /dev/null +++ b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts @@ -0,0 +1,89 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +import {MEGOLM_ALGORITHM} from "../../common"; +import type {RoomKey} from "../decryption/RoomKey"; + +import type {BaseBackupInfo, SignatureMap, SessionKeyInfo} from "./KeyBackup"; +import type * as OlmNamespace from "@matrix-org/olm"; +type Olm = typeof OlmNamespace; + +export const Algorithm = "m.megolm_backup.v1.curve25519-aes-sha2"; + +export type BackupInfo = BaseBackupInfo & { + algorithm: typeof Algorithm, + auth_data: AuthData, +} + +export type AuthData = { + public_key: string, + signatures: SignatureMap +} + +export type SessionData = { + ciphertext: string, + mac: string, + ephemeral: string, +} + +export class BackupEncryption { + constructor( + private readonly encryption: Olm.PkEncryption, + private readonly decryption: Olm.PkDecryption + ) {} + + static fromAuthData(authData: AuthData, privateKey: Uint8Array, olm: Olm): BackupEncryption { + const expectedPubKey = authData.public_key; + const decryption = new olm.PkDecryption(); + const encryption = new olm.PkEncryption(); + try { + const pubKey = decryption.init_with_private_key(privateKey); + if (pubKey !== expectedPubKey) { + throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); + } + encryption.set_recipient_key(pubKey); + } catch(err) { + decryption.free(); + throw err; + } + return new BackupEncryption(encryption, decryption); + } + + decryptRoomKey(sessionData: SessionData): SessionKeyInfo { + const sessionInfo = this.decryption.decrypt( + sessionData.ephemeral, + sessionData.mac, + sessionData.ciphertext, + ); + return JSON.parse(sessionInfo) as SessionKeyInfo; + } + + encryptRoomKey(key: RoomKey, sessionKey: string): SessionData { + const sessionInfo: SessionKeyInfo = { + algorithm: MEGOLM_ALGORITHM, + sender_key: key.senderKey, + sender_claimed_keys: {ed25519: key.claimedEd25519Key}, + forwarding_curve25519_key_chain: [], + session_key: sessionKey + }; + return this.encryption.encrypt(JSON.stringify(sessionInfo)) as SessionData; + } + + dispose() { + this.decryption.free(); + this.encryption.free(); + } +} diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 98456973..dc541cf2 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -15,9 +15,9 @@ limitations under the License. */ import {StoreNames} from "../../../storage/common"; -import {LRUCache} from "../../../../utils/LRUCache"; import {keyFromStorage, keyFromBackup} from "../decryption/RoomKey"; import {MEGOLM_ALGORITHM} from "../../common"; +import * as Curve25519 from "./Curve25519"; import type {HomeServerApi} from "../../../net/HomeServerApi"; import type {IncomingRoomKey, RoomKey} from "../decryption/RoomKey"; @@ -31,48 +31,28 @@ import type {Transaction} from "../../../storage/idb/Transaction"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; -type SignatureMap = { +export type SignatureMap = { [userId: string]: {[deviceIdAndAlgorithm: string]: string} } -interface BaseBackupInfo { +export type BaseBackupInfo = { version: string, etag: string, count: number, } -const Curve25519Algorithm = "m.megolm_backup.v1.curve25519-aes-sha2"; - -interface Curve25519BackupInfo extends BaseBackupInfo { - algorithm: typeof Curve25519Algorithm, - auth_data: Curve25519AuthData, -} - -interface OtherBackupInfo extends BaseBackupInfo { +type OtherBackupInfo = BaseBackupInfo & { algorithm: "other" }; -type BackupInfo = Curve25519BackupInfo | OtherBackupInfo; - - -interface Curve25519AuthData { - public_key: string, - signatures: SignatureMap -} - -type AuthData = Curve25519AuthData; +type BackupInfo = Curve25519.BackupInfo | OtherBackupInfo; +type AuthData = Curve25519.AuthData; type SessionInfo = { first_message_index: number, forwarded_count: number, is_verified: boolean, - session_data: Curve29915SessionData | any -} - -type Curve29915SessionData = { - ciphertext: string, - mac: string, - ephemeral: string, + session_data: Curve25519.SessionData | any } type MegOlmSessionKeyInfo = { @@ -83,12 +63,20 @@ type MegOlmSessionKeyInfo = { session_key: string } -type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; +export type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; + +type KeyBackupPayload = { + rooms: { + [roomId: string]: { + sessions: {[sessionId: string]: SessionInfo} + } + } +} export class KeyBackup { constructor( private readonly backupInfo: BackupInfo, - private readonly algorithm: Curve25519, + private readonly crypto: Curve25519.BackupEncryption, private readonly hsApi: HomeServerApi, private readonly keyLoader: KeyLoader, private readonly storage: Storage, @@ -100,7 +88,7 @@ export class KeyBackup { if (!sessionResponse.session_data) { return; } - const sessionKeyInfo = this.algorithm.decryptRoomKey(sessionResponse.session_data); + const sessionKeyInfo = this.crypto.decryptRoomKey(sessionResponse.session_data as Curve25519.SessionData); if (sessionKeyInfo?.algorithm === MEGOLM_ALGORITHM) { return keyFromBackup(roomId, sessionId, sessionKeyInfo); } else if (sessionKeyInfo?.algorithm) { @@ -131,13 +119,7 @@ export class KeyBackup { return; } const roomKeys = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn))); - const payload: { - rooms: { - [roomId: string]: { - sessions: {[sessionId: string]: SessionInfo} - } - } - } = { rooms: {} }; + const payload: KeyBackupPayload = { rooms: {} }; const payloadRooms = payload.rooms; for (const key of roomKeys) { if (key) { @@ -174,7 +156,7 @@ export class KeyBackup { first_message_index: firstMessageIndex, forwarded_count: 0, is_verified: false, - session_data: this.algorithm.encryptRoomKey(roomKey, sessionKey) + session_data: this.crypto.encryptRoomKey(roomKey, sessionKey) }; }); } @@ -184,7 +166,7 @@ export class KeyBackup { } dispose() { - this.algorithm.dispose(); + this.crypto.dispose(); } static async fromSecretStorage(platform: Platform, olm: Olm, secretStorage: SecretStorage, hsApi: HomeServerApi, keyLoader: KeyLoader, storage: Storage, txn: Transaction): Promise { @@ -192,61 +174,12 @@ export class KeyBackup { if (base64PrivateKey) { const privateKey = new Uint8Array(platform.encoding.base64.decode(base64PrivateKey)); const backupInfo = await hsApi.roomKeysVersion().response() as BackupInfo; - if (backupInfo.algorithm === Curve25519Algorithm) { - const algorithm = Curve25519.fromAuthData(backupInfo.auth_data, privateKey, olm); - return new KeyBackup(backupInfo, algorithm, hsApi, keyLoader, storage, platform); + if (backupInfo.algorithm === Curve25519.Algorithm) { + const crypto = Curve25519.BackupEncryption.fromAuthData(backupInfo.auth_data, privateKey, olm); + return new KeyBackup(backupInfo, crypto, hsApi, keyLoader, storage, platform); } else { throw new Error(`Unknown backup algorithm: ${backupInfo.algorithm}`); } } } } - -class Curve25519 { - constructor( - private readonly encryption: Olm.PkEncryption, - private readonly decryption: Olm.PkDecryption - ) {} - - static fromAuthData(authData: Curve25519AuthData, privateKey: Uint8Array, olm: Olm): Curve25519 { - const expectedPubKey = authData.public_key; - const decryption = new olm.PkDecryption(); - const encryption = new olm.PkEncryption(); - try { - const pubKey = decryption.init_with_private_key(privateKey); - if (pubKey !== expectedPubKey) { - throw new Error(`Bad backup key, public key does not match. Calculated ${pubKey} but expected ${expectedPubKey}`); - } - encryption.set_recipient_key(pubKey); - } catch(err) { - decryption.free(); - throw err; - } - return new Curve25519(encryption, decryption); - } - - decryptRoomKey(sessionData: Curve29915SessionData): SessionKeyInfo { - const sessionInfo = this.decryption.decrypt( - sessionData.ephemeral, - sessionData.mac, - sessionData.ciphertext, - ); - return JSON.parse(sessionInfo) as SessionKeyInfo; - } - - encryptRoomKey(key: RoomKey, sessionKey: string): Curve29915SessionData { - const sessionInfo: SessionKeyInfo = { - algorithm: MEGOLM_ALGORITHM, - sender_key: key.senderKey, - sender_claimed_keys: {ed25519: key.claimedEd25519Key}, - forwarding_curve25519_key_chain: [], - session_key: sessionKey - }; - return this.encryption.encrypt(JSON.stringify(sessionInfo)) as Curve29915SessionData; - } - - dispose() { - this.decryption.free(); - this.encryption.free(); - } -} From 85155a43bbadde96f4239181401f3b394debd75a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 10:17:31 +0100 Subject: [PATCH 011/160] cleanup types --- src/matrix/e2ee/megolm/keybackup/Curve25519.ts | 2 +- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts index 9a453b5a..f1345bc6 100644 --- a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts +++ b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts @@ -28,7 +28,7 @@ export type BackupInfo = BaseBackupInfo & { auth_data: AuthData, } -export type AuthData = { +type AuthData = { public_key: string, signatures: SignatureMap } diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index dc541cf2..f4c7b28b 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -24,7 +24,6 @@ import type {IncomingRoomKey, RoomKey} from "../decryption/RoomKey"; import type {KeyLoader} from "../decryption/KeyLoader"; import type {SecretStorage} from "../../../ssss/SecretStorage"; import type {Storage} from "../../../storage/idb/Storage"; -import type {DeviceIdentity} from "../../../storage/idb/stores/DeviceIdentityStore"; import type {ILogItem} from "../../../../logging/types"; import type {Platform} from "../../../../platform/web/Platform"; import type {Transaction} from "../../../storage/idb/Transaction"; @@ -46,13 +45,13 @@ type OtherBackupInfo = BaseBackupInfo & { }; type BackupInfo = Curve25519.BackupInfo | OtherBackupInfo; -type AuthData = Curve25519.AuthData; +type SessionData = Curve25519.SessionData; type SessionInfo = { first_message_index: number, forwarded_count: number, is_verified: boolean, - session_data: Curve25519.SessionData | any + session_data: SessionData } type MegOlmSessionKeyInfo = { @@ -63,6 +62,7 @@ type MegOlmSessionKeyInfo = { session_key: string } +// the type that session_data decrypts from / encrypts to export type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; type KeyBackupPayload = { From a791641b3418420f025066eea1293167f18a9027 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 12:10:20 +0100 Subject: [PATCH 012/160] move types to separate file --- .../e2ee/megolm/keybackup/Curve25519.ts | 2 +- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 43 ------------- src/matrix/e2ee/megolm/keybackup/types.ts | 61 +++++++++++++++++++ 3 files changed, 62 insertions(+), 44 deletions(-) create mode 100644 src/matrix/e2ee/megolm/keybackup/types.ts diff --git a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts index f1345bc6..8007d422 100644 --- a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts +++ b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts @@ -17,7 +17,7 @@ limitations under the License. import {MEGOLM_ALGORITHM} from "../../common"; import type {RoomKey} from "../decryption/RoomKey"; -import type {BaseBackupInfo, SignatureMap, SessionKeyInfo} from "./KeyBackup"; +import type {BaseBackupInfo, SignatureMap, SessionKeyInfo} from "./types"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index f4c7b28b..38faa5e2 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -30,49 +30,6 @@ import type {Transaction} from "../../../storage/idb/Transaction"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; -export type SignatureMap = { - [userId: string]: {[deviceIdAndAlgorithm: string]: string} -} - -export type BaseBackupInfo = { - version: string, - etag: string, - count: number, -} - -type OtherBackupInfo = BaseBackupInfo & { - algorithm: "other" -}; - -type BackupInfo = Curve25519.BackupInfo | OtherBackupInfo; -type SessionData = Curve25519.SessionData; - -type SessionInfo = { - first_message_index: number, - forwarded_count: number, - is_verified: boolean, - session_data: SessionData -} - -type MegOlmSessionKeyInfo = { - algorithm: MEGOLM_ALGORITHM, - sender_key: string, - sender_claimed_keys: {[algorithm: string]: string}, - forwarding_curve25519_key_chain: string[], - session_key: string -} - -// the type that session_data decrypts from / encrypts to -export type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; - -type KeyBackupPayload = { - rooms: { - [roomId: string]: { - sessions: {[sessionId: string]: SessionInfo} - } - } -} - export class KeyBackup { constructor( private readonly backupInfo: BackupInfo, diff --git a/src/matrix/e2ee/megolm/keybackup/types.ts b/src/matrix/e2ee/megolm/keybackup/types.ts new file mode 100644 index 00000000..ce56cca7 --- /dev/null +++ b/src/matrix/e2ee/megolm/keybackup/types.ts @@ -0,0 +1,61 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +import type * as Curve25519 from "./Curve25519"; +import type {MEGOLM_ALGORITHM} from "../../common"; + +export type SignatureMap = { + [userId: string]: {[deviceIdAndAlgorithm: string]: string} +} + +export type BaseBackupInfo = { + version: string, + etag: string, + count: number, +} + +export type OtherBackupInfo = BaseBackupInfo & { + algorithm: "other" +}; + +export type BackupInfo = Curve25519.BackupInfo | OtherBackupInfo; +export type SessionData = Curve25519.SessionData; + +export type SessionInfo = { + first_message_index: number, + forwarded_count: number, + is_verified: boolean, + session_data: SessionData +} + +export type MegOlmSessionKeyInfo = { + algorithm: MEGOLM_ALGORITHM, + sender_key: string, + sender_claimed_keys: {[algorithm: string]: string}, + forwarding_curve25519_key_chain: string[], + session_key: string +} + +// the type that session_data decrypts from / encrypts to +export type SessionKeyInfo = MegOlmSessionKeyInfo | {algorithm: string}; + +export type KeyBackupPayload = { + rooms: { + [roomId: string]: { + sessions: {[sessionId: string]: SessionInfo} + } + } +} From 524090e27d33b5700fd639cb46f87c8b22a4384c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 15:12:11 +0100 Subject: [PATCH 013/160] support idb store/index.count --- src/matrix/storage/idb/QueryTarget.ts | 5 +++++ src/matrix/storage/idb/Store.ts | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/matrix/storage/idb/QueryTarget.ts b/src/matrix/storage/idb/QueryTarget.ts index 97b85569..f27123d5 100644 --- a/src/matrix/storage/idb/QueryTarget.ts +++ b/src/matrix/storage/idb/QueryTarget.ts @@ -37,6 +37,7 @@ interface QueryTargetInterface { openKeyCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest; supports(method: string): boolean; keyPath: string | string[]; + count(keyRange?: IDBKeyRange): IDBRequest; get(key: IDBValidKey | IDBKeyRange): IDBRequest; getKey(key: IDBValidKey | IDBKeyRange): IDBRequest; } @@ -78,6 +79,10 @@ export class QueryTarget { return this._target.supports(methodName); } + count(keyRange?: IDBKeyRange): Promise { + return reqAsPromise(this._target.count(keyRange)); + } + get(key: IDBValidKey | IDBKeyRange): Promise { return reqAsPromise(this._target.get(key)); } diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts index f45b3f3e..20e6f79a 100644 --- a/src/matrix/storage/idb/Store.ts +++ b/src/matrix/storage/idb/Store.ts @@ -118,6 +118,14 @@ export class QueryTargetWrapper { } } + count(keyRange?: IDBKeyRange): IDBRequest { + try { + return this._qtStore.count(keyRange); + } catch(err) { + throw new IDBRequestAttemptError("count", this._qt, err, [keyRange]); + } + } + index(name: string): IDBIndex { try { return this._qtStore.index(name); From 554aa45d482f3b00471291234efdadf2237b983f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 15:18:23 +0100 Subject: [PATCH 014/160] add support for progress notifications in abortable operation --- src/utils/AbortableOperation.ts | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/utils/AbortableOperation.ts b/src/utils/AbortableOperation.ts index d03f820a..fba71a8c 100644 --- a/src/utils/AbortableOperation.ts +++ b/src/utils/AbortableOperation.ts @@ -14,27 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {BaseObservableValue, ObservableValue} from "../observable/ObservableValue"; + export interface IAbortable { abort(); } -type RunFn = (setAbortable: (a: IAbortable) => typeof a) => T; +export type SetAbortableFn = (a: IAbortable) => typeof a; +export type SetProgressFn

= (progress: P) => void; +type RunFn = (setAbortable: SetAbortableFn, setProgress: SetProgressFn

) => T; -export class AbortableOperation { +export class AbortableOperation implements IAbortable { public readonly result: T; - private _abortable: IAbortable | null; + private _abortable?: IAbortable; + private _progress: ObservableValue

; - constructor(run: RunFn) { - this._abortable = null; - const setAbortable = abortable => { + constructor(run: RunFn) { + this._abortable = undefined; + const setAbortable: SetAbortableFn = abortable => { this._abortable = abortable; return abortable; }; - this.result = run(setAbortable); + this._progress = new ObservableValue

(undefined); + const setProgress: SetProgressFn

= (progress: P) => { + this._progress.set(progress); + }; + this.result = run(setAbortable, setProgress); + } + + get progress(): BaseObservableValue

{ + return this._progress; } abort() { this._abortable?.abort(); - this._abortable = null; + this._abortable = undefined; } } From 60ed276b8a506b4a8467083d98b206fcd0ae442e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 15:19:31 +0100 Subject: [PATCH 015/160] add progress notification and cancellation to key backup flush --- src/matrix/Session.js | 23 +++- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 105 ++++++++++++------ .../idb/stores/SessionNeedingBackupStore.ts | 4 + 3 files changed, 93 insertions(+), 39 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 9510cbfd..d8595d12 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -76,6 +76,7 @@ export class Session { this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; this._keyBackup = null; + this._keyBackupOperation = new ObservableValue(null); this._hasSecretStorageKey = new ObservableValue(null); this._observedRoomStatus = new Map(); @@ -270,6 +271,10 @@ export class Session { return this._keyBackup; } + get keyBackupOperation() { + return this._keyBackupOperation; + } + get hasIdentity() { return !!this._e2eeAccount; } @@ -559,7 +564,7 @@ export class Session { async writeSync(syncResponse, syncFilterId, preparation, txn, log) { const changes = { syncInfo: null, - e2eeAccountChanges: null, + e2eeAccountChanges: null }; const syncToken = syncResponse.next_batch; if (syncToken !== this.syncToken) { @@ -584,7 +589,7 @@ export class Session { // this should come after the deviceMessageHandler, so the room keys are already written and their // isBetter property has been checked if (this._keyBackup) { - this._keyBackup.writeKeys(preparation.newRoomKeys, txn, log); + changes.shouldFlushKeyBackup = this._keyBackup.writeKeys(preparation.newRoomKeys, txn, log); } } @@ -623,8 +628,18 @@ export class Session { await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, false, log)); } } - if (this._keyBackup) { - this._keyBackup.flush(); + // should flush and not already flushing + if (changes.shouldFlushKeyBackup && this._keyBackup && !this._keyBackupOperation.get()) { + log.wrapDetached("flush key backup", async log => { + const operation = this._keyBackup.flush(log); + this._keyBackupOperation.set(operation); + try { + await operation.result; + } catch (err) { + log.catch(err); + } + this._keyBackupOperation.set(null); + }); } } diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 38faa5e2..cd09423c 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -18,7 +18,10 @@ import {StoreNames} from "../../../storage/common"; import {keyFromStorage, keyFromBackup} from "../decryption/RoomKey"; import {MEGOLM_ALGORITHM} from "../../common"; import * as Curve25519 from "./Curve25519"; +import {AbortableOperation} from "../../../../utils/AbortableOperation"; +import {SetAbortableFn} from "../../../../utils/AbortableOperation"; +import type {BackupInfo, SessionData, SessionKeyInfo, SessionInfo, KeyBackupPayload} from "./types"; import type {HomeServerApi} from "../../../net/HomeServerApi"; import type {IncomingRoomKey, RoomKey} from "../decryption/RoomKey"; import type {KeyLoader} from "../decryption/KeyLoader"; @@ -27,6 +30,7 @@ import type {Storage} from "../../../storage/idb/Storage"; import type {ILogItem} from "../../../../logging/types"; import type {Platform} from "../../../../platform/web/Platform"; import type {Transaction} from "../../../storage/idb/Transaction"; +import type {BackupEntry} from "../../../storage/idb/stores/SessionNeedingBackupStore"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; @@ -45,7 +49,7 @@ export class KeyBackup { if (!sessionResponse.session_data) { return; } - const sessionKeyInfo = this.crypto.decryptRoomKey(sessionResponse.session_data as Curve25519.SessionData); + const sessionKeyInfo = this.crypto.decryptRoomKey(sessionResponse.session_data as SessionData); if (sessionKeyInfo?.algorithm === MEGOLM_ALGORITHM) { return keyFromBackup(roomId, sessionId, sessionKeyInfo); } else if (sessionKeyInfo?.algorithm) { @@ -64,45 +68,69 @@ export class KeyBackup { return hasBetter; } - async flush() { - while (true) { - await this.platform.clock.createTimeout(this.platform.random() * 10000).elapsed(); - const txn = await this.storage.readTxn([ - StoreNames.sessionsNeedingBackup, - StoreNames.inboundGroupSessions, - ]); - const keysNeedingBackup = await txn.sessionsNeedingBackup.getFirstEntries(20); - if (keysNeedingBackup.length === 0) { - return; - } - const roomKeys = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn))); - const payload: KeyBackupPayload = { rooms: {} }; - const payloadRooms = payload.rooms; - for (const key of roomKeys) { - if (key) { - let roomPayload = payloadRooms[key.roomId]; - if (!roomPayload) { - roomPayload = payloadRooms[key.roomId] = { sessions: {} }; - } - roomPayload.sessions[key.sessionId] = await this.encodeRoomKey(key); - } - } - await this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload).response(); - { - const txn = await this.storage.readWriteTxn([ + // TODO: protect against having multiple concurrent flushes + flush(log: ILogItem): AbortableOperation, Progress> { + return new AbortableOperation(async (setAbortable, setProgress) => { + let total = 0; + let amountFinished = 0; + while (true) { + const timeout = this.platform.clock.createTimeout(this.platform.random() * 10000); + setAbortable(timeout); + await timeout.elapsed(); + const txn = await this.storage.readTxn([ StoreNames.sessionsNeedingBackup, + StoreNames.inboundGroupSessions, ]); - try { - for (const key of keysNeedingBackup) { - txn.sessionsNeedingBackup.remove(key.roomId, key.senderKey, key.sessionId); - } - } catch (err) { - txn.abort(); - throw err; + setAbortable(txn); + // fetch total again on each iteration as while we are flushing, sync might be adding keys + total = await txn.sessionsNeedingBackup.count(); + setProgress(new Progress(total, amountFinished)); + const keysNeedingBackup = await txn.sessionsNeedingBackup.getFirstEntries(20); + if (keysNeedingBackup.length === 0) { + return; } - await txn.complete(); + const roomKeysOrNotFound = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn))); + const roomKeys = roomKeysOrNotFound.filter(k => !!k) as RoomKey[]; + if (roomKeys.length) { + const payload = await this.encodeKeysForBackup(roomKeys); + const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload); + setAbortable(uploadRequest); + await uploadRequest.response(); + } + this.removeBackedUpKeys(keysNeedingBackup, setAbortable); + amountFinished += keysNeedingBackup.length; + setProgress(new Progress(total, amountFinished)); } + }); + } + + private async encodeKeysForBackup(roomKeys: RoomKey[]): Promise { + const payload: KeyBackupPayload = { rooms: {} }; + const payloadRooms = payload.rooms; + for (const key of roomKeys) { + let roomPayload = payloadRooms[key.roomId]; + if (!roomPayload) { + roomPayload = payloadRooms[key.roomId] = { sessions: {} }; + } + roomPayload.sessions[key.sessionId] = await this.encodeRoomKey(key); } + return payload; + } + + private async removeBackedUpKeys(keysNeedingBackup: BackupEntry[], setAbortable: SetAbortableFn) { + const txn = await this.storage.readWriteTxn([ + StoreNames.sessionsNeedingBackup, + ]); + setAbortable(txn); + try { + for (const key of keysNeedingBackup) { + txn.sessionsNeedingBackup.remove(key.roomId, key.senderKey, key.sessionId); + } + } catch (err) { + txn.abort(); + throw err; + } + await txn.complete(); } private async encodeRoomKey(roomKey: RoomKey): Promise { @@ -140,3 +168,10 @@ export class KeyBackup { } } } + +export class Progress { + constructor( + public readonly total: number, + public readonly finished: number + ) {} +} diff --git a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts index 27104191..71a383bb 100644 --- a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts +++ b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts @@ -53,4 +53,8 @@ export class SessionNeedingBackupStore { remove(roomId: string, senderKey: string, sessionId: string): void { this.store.delete(encodeKey(roomId, senderKey, sessionId)); } + + count(): Promise { + return this.store.count(); + } } From e80acd4d575c17ee3df2c2c7b5dc692a1d341450 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Jan 2022 16:30:40 +0100 Subject: [PATCH 016/160] add migration when backup is enabled --- src/matrix/storage/idb/schema.ts | 52 ++++++++++++++----- .../idb/stores/SessionNeedingBackupStore.ts | 2 +- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 2aca60a2..2da4d65c 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -6,6 +6,8 @@ import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js"; import {SESSION_E2EE_KEY_PREFIX} from "../../e2ee/common.js"; import {SummaryData} from "../../room/RoomSummary"; import {RoomMemberStore, MemberData} from "./stores/RoomMemberStore"; +import {encodeKey as encodeBackupKey} from "./stores/SessionNeedingBackupStore"; +import {InboundGroupSessionStore, InboundGroupSessionEntry} from "./stores/InboundGroupSessionStore"; import {RoomStateEntry} from "./stores/RoomStateStore"; import {SessionStore} from "./stores/SessionStore"; import {Store} from "./Store"; @@ -39,6 +41,21 @@ export const schema: MigrationFunc[] = [ // TypeScript note: for now, do not bother introducing interfaces / alias // for old schemas. Just take them as `any`. +function createDatabaseNameHelper(db: IDBDatabase): ITransaction { + // the Store object gets passed in several things through the Transaction class (a wrapper around IDBTransaction), + // the only thing we should need here is the databaseName though, so we mock it out. + // ideally we should have an easier way to go from the idb primitive layer to the specific store classes where + // we implement logic, but for now we need this. + const databaseNameHelper: ITransaction = { + databaseName: db.name, + get idbFactory(): IDBFactory { throw new Error("unused");}, + get IDBKeyRange(): typeof IDBKeyRange { throw new Error("unused");}, + addWriteError() {}, + }; + return databaseNameHelper; +} + + // how do we deal with schema updates vs existing data migration in a way that //v1 function createInitialStores(db: IDBDatabase): void { @@ -223,17 +240,7 @@ async function changeSSSSKeyPrefix(db: IDBDatabase, txn: IDBTransaction) { // v13 async function backupAndRestoreE2EEAccountToLocalStorage(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem) { const session = txn.objectStore("session"); - // the Store object gets passed in several things through the Transaction class (a wrapper around IDBTransaction), - // the only thing we should need here is the databaseName though, so we mock it out. - // ideally we should have an easier way to go from the idb primitive layer to the specific store classes where - // we implement logic, but for now we need this. - const databaseNameHelper: ITransaction = { - databaseName: db.name, - get idbFactory(): IDBFactory { throw new Error("unused");}, - get IDBKeyRange(): typeof IDBKeyRange { throw new Error("unused");}, - addWriteError() {}, - }; - const sessionStore = new SessionStore(new Store(session, databaseNameHelper), localStorage); + const sessionStore = new SessionStore(new Store(session, createDatabaseNameHelper(db)), localStorage); // if we already have an e2ee identity, write a backup to local storage. // further updates to e2ee keys in the session store will also write to local storage from 0.2.15 on, // but here we make sure a backup is immediately created after installing the update and we don't wait until @@ -273,6 +280,25 @@ async function clearAllStores(db: IDBDatabase, txn: IDBTransaction) { } // v15 adds the sessionsNeedingBackup store, for key backup -function createSessionsNeedingBackup(db: IDBDatabase): void { - db.createObjectStore("sessionsNeedingBackup", {keyPath: "key"}); +async function createSessionsNeedingBackup(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem): Promise { + const backupStore = db.createObjectStore("sessionsNeedingBackup", {keyPath: "key"}); + const session = txn.objectStore("session"); + const ssssKey = await reqAsPromise(session.get(`${SESSION_E2EE_KEY_PREFIX}ssssKey`) as IDBRequest); + const keyBackupEnabled = !!ssssKey; + log.set("key backup", keyBackupEnabled); + if (keyBackupEnabled) { + let count = 0; + try { + const inboundGroupSessions = txn.objectStore("inboundGroupSessions"); + await iterateCursor(inboundGroupSessions.openCursor(), session => { + backupStore.add({key: encodeBackupKey(session.roomId, session.senderKey, session.sessionId)}); + count += 1; + return NOT_DONE; + }); + } catch (err) { + txn.abort(); + log.log("could not migrate operations").catch(err); + } + log.set("count", count); + } } diff --git a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts index 71a383bb..327fce9a 100644 --- a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts +++ b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts @@ -26,7 +26,7 @@ type StorageEntry = { key: string }; -function encodeKey(roomId: string, senderKey: string, sessionId: string): string { +export function encodeKey(roomId: string, senderKey: string, sessionId: string): string { return `${roomId}|${senderKey}|${sessionId}`; } From bf08c0d85066b6967b927b680489f8652f599065 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Jan 2022 14:19:37 +0100 Subject: [PATCH 017/160] deal with errors when enabling key backup fixes #449 --- src/matrix/Session.js | 82 +++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index d8595d12..5e68829b 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -190,22 +190,25 @@ export class Session { * @param {string} credential either the passphrase or the recovery key, depending on the type * @return {Promise} resolves or rejects after having tried to enable secret storage */ - async enableSecretStorage(type, credential) { - if (!this._olm) { - throw new Error("olm required"); - } - if (this._keyBackup) { - return false; - } - const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); - // and create key backup, which needs to read from accountData - const readTxn = await this._storage.readTxn([ - this._storage.storeNames.accountData, - ]); - await this._createKeyBackup(key, readTxn); - await this._writeSSSSKey(key); - this._hasSecretStorageKey.set(true); - return key; + enableSecretStorage(type, credential, log = undefined) { + return this._platform.logger.wrapOrRun(log, "enable secret storage", async log => { + if (!this._olm) { + throw new Error("olm required"); + } + if (this._keyBackup) { + return false; + } + const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); + // and create key backup, which needs to read from accountData + const readTxn = await this._storage.readTxn([ + this._storage.storeNames.accountData, + ]); + if (await this._createKeyBackup(key, readTxn, log)) { + await this._writeSSSSKey(key); + this._hasSecretStorageKey.set(true); + return key; + } + }); } async _writeSSSSKey(key) { @@ -246,25 +249,33 @@ export class Session { this._hasSecretStorageKey.set(false); } - async _createKeyBackup(ssssKey, txn) { - const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); - this._keyBackup = await KeyBackup.fromSecretStorage( - this._platform, - this._olm, - secretStorage, - this._hsApi, - this._keyLoader, - this._storage, - txn - ); - if (this._keyBackup) { - for (const room of this._rooms.values()) { - if (room.isEncrypted) { - room.enableKeyBackup(this._keyBackup); + _createKeyBackup(ssssKey, txn, log) { + return log.wrap("enable key backup", async log => { + try { + const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); + this._keyBackup = await KeyBackup.fromSecretStorage( + this._platform, + this._olm, + secretStorage, + this._hsApi, + this._keyLoader, + this._storage, + txn + ); + if (this._keyBackup) { + for (const room of this._rooms.values()) { + if (room.isEncrypted) { + room.enableKeyBackup(this._keyBackup); + } + } } + this.needsKeyBackup.set(false); + } catch (err) { + log.catch(err); + return false; } - } - this.needsKeyBackup.set(false); + return true; + }); } get keyBackup() { @@ -455,11 +466,12 @@ export class Session { ]); // try set up session backup if we stored the ssss key const ssssKey = await ssssReadKey(txn); + let couldReadKeyBackup = false; if (ssssKey) { // txn will end here as this does a network request - await this._createKeyBackup(ssssKey, txn); + couldReadKeyBackup = await this._createKeyBackup(ssssKey, txn, log); } - this._hasSecretStorageKey.set(!!ssssKey); + this._hasSecretStorageKey.set(couldReadKeyBackup); } // restore unfinished operations, like sending out room keys const opsTxn = await this._storage.readWriteTxn([ From 0b4954a9caa4750aa21dbce555ef28d9c08fea9f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Jan 2022 14:20:04 +0100 Subject: [PATCH 018/160] log key backup upload requests --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index cd09423c..c5fc7210 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -93,7 +93,7 @@ export class KeyBackup { const roomKeys = roomKeysOrNotFound.filter(k => !!k) as RoomKey[]; if (roomKeys.length) { const payload = await this.encodeKeysForBackup(roomKeys); - const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload); + const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log}); setAbortable(uploadRequest); await uploadRequest.response(); } From 6f1484005bd883b7f4a32def52d553b6bb9deb11 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Jan 2022 15:14:29 +0100 Subject: [PATCH 019/160] stop key backup when on the wrong version users can then enter the new key in the settings to start backing up again --- src/matrix/Session.js | 7 ++++++- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 5e68829b..9b9d013e 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -646,7 +646,12 @@ export class Session { const operation = this._keyBackup.flush(log); this._keyBackupOperation.set(operation); try { - await operation.result; + const success = await operation.result; + // stop key backup if the version was changed + if (!success) { + this._keyBackup = this._keyBackup.dispose(); + this.needsKeyBackup.set(true); + } } catch (err) { log.catch(err); } diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index c5fc7210..3792ea96 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -69,7 +69,7 @@ export class KeyBackup { } // TODO: protect against having multiple concurrent flushes - flush(log: ILogItem): AbortableOperation, Progress> { + flush(log: ILogItem): AbortableOperation, Progress> { return new AbortableOperation(async (setAbortable, setProgress) => { let total = 0; let amountFinished = 0; @@ -87,7 +87,7 @@ export class KeyBackup { setProgress(new Progress(total, amountFinished)); const keysNeedingBackup = await txn.sessionsNeedingBackup.getFirstEntries(20); if (keysNeedingBackup.length === 0) { - return; + return true; } const roomKeysOrNotFound = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn))); const roomKeys = roomKeysOrNotFound.filter(k => !!k) as RoomKey[]; @@ -95,7 +95,16 @@ export class KeyBackup { const payload = await this.encodeKeysForBackup(roomKeys); const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log}); setAbortable(uploadRequest); - await uploadRequest.response(); + try { + await uploadRequest.response(); + } catch (err) { + if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { + log.set("wrong_version", true); + return false; + } else { + throw err; + } + } } this.removeBackedUpKeys(keysNeedingBackup, setAbortable); amountFinished += keysNeedingBackup.length; From 48e72f9b69e44069bcf97f00d7a9298d6b52a0a6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Jan 2022 16:00:46 +0100 Subject: [PATCH 020/160] replace SessionsNeedingBackup store with backup field on inbound session --- src/matrix/storage/common.ts | 1 - src/matrix/storage/idb/Transaction.ts | 5 -- src/matrix/storage/idb/schema.ts | 35 ++++------- .../idb/stores/InboundGroupSessionStore.ts | 21 +++++++ .../idb/stores/SessionNeedingBackupStore.ts | 60 ------------------- 5 files changed, 31 insertions(+), 91 deletions(-) delete mode 100644 src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts diff --git a/src/matrix/storage/common.ts b/src/matrix/storage/common.ts index 3929c061..23bb0d31 100644 --- a/src/matrix/storage/common.ts +++ b/src/matrix/storage/common.ts @@ -33,7 +33,6 @@ export enum StoreNames { groupSessionDecryptions = "groupSessionDecryptions", operations = "operations", accountData = "accountData", - sessionsNeedingBackup = "sessionsNeedingBackup", } export const STORE_NAMES: Readonly = Object.values(StoreNames); diff --git a/src/matrix/storage/idb/Transaction.ts b/src/matrix/storage/idb/Transaction.ts index 9b765310..80894105 100644 --- a/src/matrix/storage/idb/Transaction.ts +++ b/src/matrix/storage/idb/Transaction.ts @@ -36,7 +36,6 @@ import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore"; import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore"; import {OperationStore} from "./stores/OperationStore"; import {AccountDataStore} from "./stores/AccountDataStore"; -import {SessionNeedingBackupStore} from "./stores/SessionNeedingBackupStore"; import type {ILogger, ILogItem} from "../../../logging/types"; export type IDBKey = IDBValidKey | IDBKeyRange; @@ -152,10 +151,6 @@ export class Transaction { get inboundGroupSessions(): InboundGroupSessionStore { return this._store(StoreNames.inboundGroupSessions, idbStore => new InboundGroupSessionStore(idbStore)); } - - get sessionsNeedingBackup(): SessionNeedingBackupStore { - return this._store(StoreNames.sessionsNeedingBackup, idbStore => new SessionNeedingBackupStore(idbStore)); - } get outboundGroupSessions(): OutboundGroupSessionStore { return this._store(StoreNames.outboundGroupSessions, idbStore => new OutboundGroupSessionStore(idbStore)); diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 2da4d65c..e6644fd0 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -6,8 +6,7 @@ import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js"; import {SESSION_E2EE_KEY_PREFIX} from "../../e2ee/common.js"; import {SummaryData} from "../../room/RoomSummary"; import {RoomMemberStore, MemberData} from "./stores/RoomMemberStore"; -import {encodeKey as encodeBackupKey} from "./stores/SessionNeedingBackupStore"; -import {InboundGroupSessionStore, InboundGroupSessionEntry} from "./stores/InboundGroupSessionStore"; +import {InboundGroupSessionStore, InboundGroupSessionEntry, BackupStatus} from "./stores/InboundGroupSessionStore"; import {RoomStateEntry} from "./stores/RoomStateStore"; import {SessionStore} from "./stores/SessionStore"; import {Store} from "./Store"; @@ -34,7 +33,7 @@ export const schema: MigrationFunc[] = [ changeSSSSKeyPrefix, backupAndRestoreE2EEAccountToLocalStorage, clearAllStores, - createSessionsNeedingBackup + addInboundSessionBackupIndex ]; // TODO: how to deal with git merge conflicts of this array? @@ -279,26 +278,12 @@ async function clearAllStores(db: IDBDatabase, txn: IDBTransaction) { } } -// v15 adds the sessionsNeedingBackup store, for key backup -async function createSessionsNeedingBackup(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem): Promise { - const backupStore = db.createObjectStore("sessionsNeedingBackup", {keyPath: "key"}); - const session = txn.objectStore("session"); - const ssssKey = await reqAsPromise(session.get(`${SESSION_E2EE_KEY_PREFIX}ssssKey`) as IDBRequest); - const keyBackupEnabled = !!ssssKey; - log.set("key backup", keyBackupEnabled); - if (keyBackupEnabled) { - let count = 0; - try { - const inboundGroupSessions = txn.objectStore("inboundGroupSessions"); - await iterateCursor(inboundGroupSessions.openCursor(), session => { - backupStore.add({key: encodeBackupKey(session.roomId, session.senderKey, session.sessionId)}); - count += 1; - return NOT_DONE; - }); - } catch (err) { - txn.abort(); - log.log("could not migrate operations").catch(err); - } - log.set("count", count); - } +// v15 add backup index to inboundGroupSessions +async function addInboundSessionBackupIndex(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem): Promise { + const inboundGroupSessions = txn.objectStore("inboundGroupSessions"); + await iterateCursor(inboundGroupSessions.openCursor(), (value, key, cursor) => { + value.backup = BackupStatus.NotBackedUp; + return NOT_DONE; + }); + inboundGroupSessions.createIndex("byBackup", "backup", {unique: false}); } diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index c670302c..72ea6e9a 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -17,6 +17,11 @@ limitations under the License. import {MIN_UNICODE, MAX_UNICODE} from "./common"; import {Store} from "../Store"; +export enum BackupStatus { + NotBackedUp = 0, + BackedUp = 1 +} + export interface InboundGroupSessionEntry { roomId: string; senderKey: string; @@ -24,6 +29,7 @@ export interface InboundGroupSessionEntry { session?: string; claimedKeys?: { [algorithm : string] : string }; eventIds?: string[]; + backup: BackupStatus } type InboundGroupSessionStorageEntry = InboundGroupSessionEntry & { key: string }; @@ -63,4 +69,19 @@ export class InboundGroupSessionStore { ); this._store.delete(range); } + countNonBackedUpSessions(): Promise { + return this._store.index("byBackup").count(); + } + + getFirstNonBackedUpSessions(amount: number): Promise { + return this._store.index("byBackup").selectLimit(0, amount); + } + + async markAsBackedUp(roomId: string, senderKey: string, sessionId: string): Promise { + const entry = await this._store.get(encodeKey(roomId, senderKey, sessionId)); + if (entry) { + entry.backup = BackupStatus.BackedUp; + this._store.put(entry); + } + } } diff --git a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts b/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts deleted file mode 100644 index 327fce9a..00000000 --- a/src/matrix/storage/idb/stores/SessionNeedingBackupStore.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -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. -*/ - -import type {Store} from "../Store"; - -export type BackupEntry = { - roomId: string; - senderKey: string; - sessionId: string; -}; - -type StorageEntry = { - key: string -}; - -export function encodeKey(roomId: string, senderKey: string, sessionId: string): string { - return `${roomId}|${senderKey}|${sessionId}`; -} - -function decodeKey(key: string): BackupEntry { - const [roomId, senderKey, sessionId] = key.split("|"); - return {roomId, senderKey, sessionId}; -} - -export class SessionNeedingBackupStore { - constructor(private store: Store) {} - - async getFirstEntries(amount: number): Promise { - const storageEntries = await this.store.selectLimit(undefined, amount); - return storageEntries.map(s => decodeKey(s.key)); - } - - set(roomId: string, senderKey: string, sessionId: string): void { - const storageEntry : StorageEntry = { - key: encodeKey(roomId, senderKey, sessionId), - }; - this.store.put(storageEntry); - } - - remove(roomId: string, senderKey: string, sessionId: string): void { - this.store.delete(encodeKey(roomId, senderKey, sessionId)); - } - - count(): Promise { - return this.store.count(); - } -} From dd2b41ff95f3527bd313d87bb1c11dbc2a5163f2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Jan 2022 16:07:18 +0100 Subject: [PATCH 021/160] use backup flag in key backup rather than separate store --- src/matrix/DeviceMessageHandler.js | 3 +- src/matrix/Session.js | 9 +-- src/matrix/e2ee/megolm/decryption/RoomKey.ts | 12 +++- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 60 +++++++------------ 4 files changed, 35 insertions(+), 49 deletions(-) diff --git a/src/matrix/DeviceMessageHandler.js b/src/matrix/DeviceMessageHandler.js index 0a606841..6ac5ac07 100644 --- a/src/matrix/DeviceMessageHandler.js +++ b/src/matrix/DeviceMessageHandler.js @@ -57,7 +57,8 @@ export class DeviceMessageHandler { async writeSync(prep, txn) { // write olm changes prep.olmDecryptChanges.write(txn); - await Promise.all(prep.newRoomKeys.map(key => this._megolmDecryption.writeRoomKey(key, txn))); + const didWriteValues = await Promise.all(prep.newRoomKeys.map(key => this._megolmDecryption.writeRoomKey(key, txn))); + return didWriteValues.some(didWrite => !!didWrite); } } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 9b9d013e..dd7a0e1c 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -597,12 +597,7 @@ export class Session { } if (preparation) { - await log.wrap("deviceMsgs", log => this._deviceMessageHandler.writeSync(preparation, txn, log)); - // this should come after the deviceMessageHandler, so the room keys are already written and their - // isBetter property has been checked - if (this._keyBackup) { - changes.shouldFlushKeyBackup = this._keyBackup.writeKeys(preparation.newRoomKeys, txn, log); - } + changes.hasNewRoomKeys = await log.wrap("deviceMsgs", log => this._deviceMessageHandler.writeSync(preparation, txn, log)); } // store account data @@ -641,7 +636,7 @@ export class Session { } } // should flush and not already flushing - if (changes.shouldFlushKeyBackup && this._keyBackup && !this._keyBackupOperation.get()) { + if (changes.hasNewRoomKeys && this._keyBackup && !this._keyBackupOperation.get()) { log.wrapDetached("flush key backup", async log => { const operation = this._keyBackup.flush(log); this._keyBackupOperation.set(operation); diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index 299b0a81..6112ccef 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {BackupStatus} from "../../../storage/idb/stores/InboundGroupSessionStore"; import type {InboundGroupSessionEntry} from "../../../storage/idb/stores/InboundGroupSessionStore"; import type {Transaction} from "../../../storage/idb/Transaction"; import type {DecryptionResult} from "../../DecryptionResult"; @@ -81,6 +82,7 @@ export abstract class IncomingRoomKey extends RoomKey { senderKey: this.senderKey, sessionId: this.sessionId, session: pickledSession, + backup: this.backupStatus, claimedKeys: {"ed25519": this.claimedEd25519Key}, }; txn.inboundGroupSessions.set(sessionEntry); @@ -125,6 +127,10 @@ export abstract class IncomingRoomKey extends RoomKey { } return this.isBetter!; } + + protected get backupStatus(): BackupStatus { + return BackupStatus.NotBackedUp; + } } class DeviceMessageRoomKey extends IncomingRoomKey { @@ -162,9 +168,13 @@ class BackupRoomKey extends IncomingRoomKey { loadInto(session) { session.import_session(this.serializationKey); } + + protected get backupStatus(): BackupStatus { + return BackupStatus.BackedUp; + } } -class StoredRoomKey extends RoomKey { +export class StoredRoomKey extends RoomKey { private storageEntry: InboundGroupSessionEntry; constructor(storageEntry: InboundGroupSessionEntry) { diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 3792ea96..49771d15 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -15,7 +15,7 @@ limitations under the License. */ import {StoreNames} from "../../../storage/common"; -import {keyFromStorage, keyFromBackup} from "../decryption/RoomKey"; +import {StoredRoomKey, keyFromBackup} from "../decryption/RoomKey"; import {MEGOLM_ALGORITHM} from "../../common"; import * as Curve25519 from "./Curve25519"; import {AbortableOperation} from "../../../../utils/AbortableOperation"; @@ -30,7 +30,6 @@ import type {Storage} from "../../../storage/idb/Storage"; import type {ILogItem} from "../../../../logging/types"; import type {Platform} from "../../../../platform/web/Platform"; import type {Transaction} from "../../../storage/idb/Transaction"; -import type {BackupEntry} from "../../../storage/idb/stores/SessionNeedingBackupStore"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; @@ -57,17 +56,6 @@ export class KeyBackup { } } - writeKeys(roomKeys: IncomingRoomKey[], txn: Transaction): boolean { - let hasBetter = false; - for (const key of roomKeys) { - if (key.isBetter) { - txn.sessionsNeedingBackup.set(key.roomId, key.senderKey, key.sessionId); - hasBetter = true; - } - } - return hasBetter; - } - // TODO: protect against having multiple concurrent flushes flush(log: ILogItem): AbortableOperation, Progress> { return new AbortableOperation(async (setAbortable, setProgress) => { @@ -77,36 +65,30 @@ export class KeyBackup { const timeout = this.platform.clock.createTimeout(this.platform.random() * 10000); setAbortable(timeout); await timeout.elapsed(); - const txn = await this.storage.readTxn([ - StoreNames.sessionsNeedingBackup, - StoreNames.inboundGroupSessions, - ]); + const txn = await this.storage.readTxn([StoreNames.inboundGroupSessions]); setAbortable(txn); // fetch total again on each iteration as while we are flushing, sync might be adding keys - total = await txn.sessionsNeedingBackup.count(); + total = await txn.inboundGroupSessions.countNonBackedUpSessions(); setProgress(new Progress(total, amountFinished)); - const keysNeedingBackup = await txn.sessionsNeedingBackup.getFirstEntries(20); + const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(20)) + .map(entry => new StoredRoomKey(entry)); if (keysNeedingBackup.length === 0) { return true; } - const roomKeysOrNotFound = await Promise.all(keysNeedingBackup.map(k => keyFromStorage(k.roomId, k.senderKey, k.sessionId, txn))); - const roomKeys = roomKeysOrNotFound.filter(k => !!k) as RoomKey[]; - if (roomKeys.length) { - const payload = await this.encodeKeysForBackup(roomKeys); - const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log}); - setAbortable(uploadRequest); - try { - await uploadRequest.response(); - } catch (err) { - if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { - log.set("wrong_version", true); - return false; - } else { - throw err; - } + const payload = await this.encodeKeysForBackup(keysNeedingBackup); + const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log}); + setAbortable(uploadRequest); + try { + await uploadRequest.response(); + } catch (err) { + if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { + log.set("wrong_version", true); + return false; + } else { + throw err; } } - this.removeBackedUpKeys(keysNeedingBackup, setAbortable); + this.markKeysAsBackedUp(keysNeedingBackup, setAbortable); amountFinished += keysNeedingBackup.length; setProgress(new Progress(total, amountFinished)); } @@ -126,15 +108,13 @@ export class KeyBackup { return payload; } - private async removeBackedUpKeys(keysNeedingBackup: BackupEntry[], setAbortable: SetAbortableFn) { + private async markKeysAsBackedUp(roomKeys: RoomKey[], setAbortable: SetAbortableFn) { const txn = await this.storage.readWriteTxn([ - StoreNames.sessionsNeedingBackup, + StoreNames.inboundGroupSessions, ]); setAbortable(txn); try { - for (const key of keysNeedingBackup) { - txn.sessionsNeedingBackup.remove(key.roomId, key.senderKey, key.sessionId); - } + await Promise.all(roomKeys.map(key => txn.inboundGroupSessions.markAsBackedUp(key.roomId, key.senderKey, key.sessionId))); } catch (err) { txn.abort(); throw err; From c81dde53e7dc473ed7f7d3d771e419e5bdfdf9e2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 10:00:41 +0100 Subject: [PATCH 022/160] store key source in inbound session --- src/matrix/e2ee/megolm/decryption/KeyLoader.ts | 5 +++++ src/matrix/e2ee/megolm/decryption/RoomKey.ts | 10 ++++++++-- src/matrix/storage/idb/schema.ts | 7 ++++++- .../storage/idb/stores/InboundGroupSessionStore.ts | 8 +++++++- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/matrix/e2ee/megolm/decryption/KeyLoader.ts b/src/matrix/e2ee/megolm/decryption/KeyLoader.ts index c1925e84..884203a3 100644 --- a/src/matrix/e2ee/megolm/decryption/KeyLoader.ts +++ b/src/matrix/e2ee/megolm/decryption/KeyLoader.ts @@ -213,6 +213,9 @@ class KeyOperation { } } +import {KeySource} from "../../../storage/idb/stores/InboundGroupSessionStore"; + + export function tests() { let instances = 0; @@ -237,6 +240,8 @@ export function tests() { get serializationKey(): string { return `key-${this.sessionId}-${this._firstKnownIndex}`; } get serializationType(): string { return "type"; } get eventIds(): string[] | undefined { return undefined; } + get keySource(): KeySource { return KeySource.DeviceMessage; } + loadInto(session: Olm.InboundGroupSession) { const mockSession = session as MockInboundSession; mockSession.sessionId = this.sessionId; diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index 6112ccef..7b648d92 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {BackupStatus} from "../../../storage/idb/stores/InboundGroupSessionStore"; +import {BackupStatus, KeySource} from "../../../storage/idb/stores/InboundGroupSessionStore"; import type {InboundGroupSessionEntry} from "../../../storage/idb/stores/InboundGroupSessionStore"; import type {Transaction} from "../../../storage/idb/Transaction"; import type {DecryptionResult} from "../../DecryptionResult"; @@ -83,6 +83,7 @@ export abstract class IncomingRoomKey extends RoomKey { sessionId: this.sessionId, session: pickledSession, backup: this.backupStatus, + source: this.keySource, claimedKeys: {"ed25519": this.claimedEd25519Key}, }; txn.inboundGroupSessions.set(sessionEntry); @@ -131,6 +132,8 @@ export abstract class IncomingRoomKey extends RoomKey { protected get backupStatus(): BackupStatus { return BackupStatus.NotBackedUp; } + + protected abstract get keySource(): KeySource; } class DeviceMessageRoomKey extends IncomingRoomKey { @@ -147,10 +150,12 @@ class DeviceMessageRoomKey extends IncomingRoomKey { get claimedEd25519Key() { return this._decryptionResult.claimedEd25519Key; } get serializationKey(): string { return this._decryptionResult.event.content?.["session_key"]; } get serializationType(): string { return "create"; } + protected get keySource(): KeySource { return KeySource.DeviceMessage; } loadInto(session) { session.create(this.serializationKey); } + } class BackupRoomKey extends IncomingRoomKey { @@ -164,7 +169,8 @@ class BackupRoomKey extends IncomingRoomKey { get claimedEd25519Key() { return this._backupInfo["sender_claimed_keys"]?.["ed25519"]; } get serializationKey(): string { return this._backupInfo["session_key"]; } get serializationType(): string { return "import_session"; } - + protected get keySource(): KeySource { return KeySource.Backup; } + loadInto(session) { session.import_session(this.serializationKey); } diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index e6644fd0..9c105a71 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -6,7 +6,7 @@ import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js"; import {SESSION_E2EE_KEY_PREFIX} from "../../e2ee/common.js"; import {SummaryData} from "../../room/RoomSummary"; import {RoomMemberStore, MemberData} from "./stores/RoomMemberStore"; -import {InboundGroupSessionStore, InboundGroupSessionEntry, BackupStatus} from "./stores/InboundGroupSessionStore"; +import {InboundGroupSessionStore, InboundGroupSessionEntry, BackupStatus, KeySource} from "./stores/InboundGroupSessionStore"; import {RoomStateEntry} from "./stores/RoomStateStore"; import {SessionStore} from "./stores/SessionStore"; import {Store} from "./Store"; @@ -283,6 +283,11 @@ async function addInboundSessionBackupIndex(db: IDBDatabase, txn: IDBTransaction const inboundGroupSessions = txn.objectStore("inboundGroupSessions"); await iterateCursor(inboundGroupSessions.openCursor(), (value, key, cursor) => { value.backup = BackupStatus.NotBackedUp; + // we'll also have backup keys in here, we can't tell, + // but the worst thing that can happen is that we try + // to backup keys that were already in backup, which + // the server will ignore + value.source = KeySource.DeviceMessage; return NOT_DONE; }); inboundGroupSessions.createIndex("byBackup", "backup", {unique: false}); diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index 72ea6e9a..96b127f7 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -22,6 +22,11 @@ export enum BackupStatus { BackedUp = 1 } +export enum KeySource { + DeviceMessage = 1, + Backup, +} + export interface InboundGroupSessionEntry { roomId: string; senderKey: string; @@ -29,7 +34,8 @@ export interface InboundGroupSessionEntry { session?: string; claimedKeys?: { [algorithm : string] : string }; eventIds?: string[]; - backup: BackupStatus + backup: BackupStatus, + source: KeySource } type InboundGroupSessionStorageEntry = InboundGroupSessionEntry & { key: string }; From a499689bd857ea5cf391eba074b9d4dc8704c512 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:07:49 +0100 Subject: [PATCH 023/160] also write room key that we create ourselves with RoomKey infrastructure so all keys are written in one place and the flags are always correct --- src/matrix/e2ee/megolm/Encryption.js | 33 +++++-------------- src/matrix/e2ee/megolm/decryption/RoomKey.ts | 31 +++++++++++++++++ .../idb/stores/InboundGroupSessionStore.ts | 1 + 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/matrix/e2ee/megolm/Encryption.js b/src/matrix/e2ee/megolm/Encryption.js index bd2311d7..eb5f68d3 100644 --- a/src/matrix/e2ee/megolm/Encryption.js +++ b/src/matrix/e2ee/megolm/Encryption.js @@ -15,12 +15,14 @@ limitations under the License. */ import {MEGOLM_ALGORITHM} from "../common.js"; +import {OutboundRoomKey} from "./decryption/RoomKey"; export class Encryption { - constructor({pickleKey, olm, account, storage, now, ownDeviceId}) { + constructor({pickleKey, olm, account, keyLoader, storage, now, ownDeviceId}) { this._pickleKey = pickleKey; this._olm = olm; this._account = account; + this._keyLoader = keyLoader; this._storage = storage; this._now = now; this._ownDeviceId = ownDeviceId; @@ -64,7 +66,7 @@ export class Encryption { let roomKeyMessage; try { let sessionEntry = await txn.outboundGroupSessions.get(roomId); - roomKeyMessage = this._readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn); + roomKeyMessage = await this._readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn); if (roomKeyMessage) { this._writeSession(this._now(), session, roomId, txn); } @@ -79,7 +81,7 @@ export class Encryption { } } - _readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn) { + async _readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn) { if (sessionEntry) { session.unpickle(this._pickleKey, sessionEntry.session); } @@ -91,7 +93,8 @@ export class Encryption { } session.create(); const roomKeyMessage = this._createRoomKeyMessage(session, roomId); - this._storeAsInboundSession(session, roomId, txn); + const roomKey = new OutboundRoomKey(roomId, session, this._account.identityKeys); + await roomKey.write(this._keyLoader, txn); return roomKeyMessage; } } @@ -123,7 +126,7 @@ export class Encryption { let encryptedContent; try { let sessionEntry = await txn.outboundGroupSessions.get(roomId); - roomKeyMessage = this._readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn); + roomKeyMessage = await this._readOrCreateSession(session, sessionEntry, roomId, encryptionParams, txn); encryptedContent = this._encryptContent(roomId, session, type, content); // update timestamp when a new session is created const createdAt = roomKeyMessage ? this._now() : sessionEntry.createdAt; @@ -190,26 +193,6 @@ export class Encryption { chain_index: session.message_index() } } - - _storeAsInboundSession(outboundSession, roomId, txn) { - const {identityKeys} = this._account; - const claimedKeys = {ed25519: identityKeys.ed25519}; - const session = new this._olm.InboundGroupSession(); - try { - session.create(outboundSession.session_key()); - const sessionEntry = { - roomId, - senderKey: identityKeys.curve25519, - sessionId: session.session_id(), - session: session.pickle(this._pickleKey), - claimedKeys, - }; - txn.inboundGroupSessions.set(sessionEntry); - return sessionEntry; - } finally { - session.free(); - } - } } /** diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index 7b648d92..f4812acc 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -155,7 +155,38 @@ class DeviceMessageRoomKey extends IncomingRoomKey { loadInto(session) { session.create(this.serializationKey); } +} +// a room key we send out ourselves, +// here adapted to write it as an incoming key +// as we don't send it to ourself with a to_device msg +export class OutboundRoomKey extends IncomingRoomKey { + private _sessionKey: string; + + constructor( + private readonly _roomId: string, + private readonly outboundSession: Olm.OutboundGroupSession, + private readonly identityKeys: {[algo: string]: string} + ) { + super(); + // this is a new key, so always better than what might be in storage, no need to check + this.isBetter = true; + // cache this, as it is used by key loader to find a matching key and + // this calls into WASM so is not just reading a prop + this._sessionKey = this.outboundSession.session_key(); + } + + get roomId(): string { return this._roomId; } + get senderKey(): string { return this.identityKeys.curve25519; } + get sessionId(): string { return this.outboundSession.session_id(); } + get claimedEd25519Key(): string { return this.identityKeys.ed25519; } + get serializationKey(): string { return this._sessionKey; } + get serializationType(): string { return "create"; } + protected get keySource(): KeySource { return KeySource.Outbound; } + + loadInto(session: Olm.InboundGroupSession) { + session.create(this.serializationKey); + } } class BackupRoomKey extends IncomingRoomKey { diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index 96b127f7..f16872a7 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -25,6 +25,7 @@ export enum BackupStatus { export enum KeySource { DeviceMessage = 1, Backup, + Outbound } export interface InboundGroupSessionEntry { From b30db544a31dab621e771c29b4dfe27975e80727 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:11:32 +0100 Subject: [PATCH 024/160] use idb key range to select non-backed up keys --- src/matrix/storage/idb/stores/InboundGroupSessionStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index f16872a7..a2ccae95 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -81,7 +81,7 @@ export class InboundGroupSessionStore { } getFirstNonBackedUpSessions(amount: number): Promise { - return this._store.index("byBackup").selectLimit(0, amount); + return this._store.index("byBackup").selectLimit(this._store.IDBKeyRange.only(BackupStatus.NotBackedUp), amount); } async markAsBackedUp(roomId: string, senderKey: string, sessionId: string): Promise { From ebc7f1ecd77a551f2cc24348bcc8253d528c42e1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:11:52 +0100 Subject: [PATCH 025/160] needs to be awaited --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 49771d15..2de71491 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -88,7 +88,7 @@ export class KeyBackup { throw err; } } - this.markKeysAsBackedUp(keysNeedingBackup, setAbortable); + await this.markKeysAsBackedUp(keysNeedingBackup, setAbortable); amountFinished += keysNeedingBackup.length; setProgress(new Progress(total, amountFinished)); } From b692b3ec4fde9a36cf74a4c9884e8bcffdd31a67 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:13:23 +0100 Subject: [PATCH 026/160] move key backup operation and flush bookkeeping inside KeyBackup so we can flush from other places than Session --- src/matrix/Session.js | 27 ++------------ src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 37 ++++++++++++++++--- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index dd7a0e1c..7209e523 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -76,7 +76,6 @@ export class Session { this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; this._keyBackup = null; - this._keyBackupOperation = new ObservableValue(null); this._hasSecretStorageKey = new ObservableValue(null); this._observedRoomStatus = new Map(); @@ -135,15 +134,16 @@ export class Session { olmUtil: this._olmUtil, senderKeyLock }); + this._keyLoader = new MegOlmKeyLoader(this._olm, PICKLE_KEY, 20); this._megolmEncryption = new MegOlmEncryption({ account: this._e2eeAccount, pickleKey: PICKLE_KEY, olm: this._olm, storage: this._storage, + keyLoader: this._keyLoader, now: this._platform.clock.now, ownDeviceId: this._sessionInfo.deviceId, }); - this._keyLoader = new MegOlmKeyLoader(this._olm, PICKLE_KEY, 20); this._megolmDecryption = new MegOlmDecryption(this._keyLoader, this._olmWorker); this._deviceMessageHandler.enableEncryption({olmDecryption, megolmDecryption: this._megolmDecryption}); } @@ -282,10 +282,6 @@ export class Session { return this._keyBackup; } - get keyBackupOperation() { - return this._keyBackupOperation; - } - get hasIdentity() { return !!this._e2eeAccount; } @@ -635,23 +631,8 @@ export class Session { await log.wrap("uploadKeys", log => this._e2eeAccount.uploadKeys(this._storage, false, log)); } } - // should flush and not already flushing - if (changes.hasNewRoomKeys && this._keyBackup && !this._keyBackupOperation.get()) { - log.wrapDetached("flush key backup", async log => { - const operation = this._keyBackup.flush(log); - this._keyBackupOperation.set(operation); - try { - const success = await operation.result; - // stop key backup if the version was changed - if (!success) { - this._keyBackup = this._keyBackup.dispose(); - this.needsKeyBackup.set(true); - } - } catch (err) { - log.catch(err); - } - this._keyBackupOperation.set(null); - }); + if (changes.hasNewRoomKeys) { + this._keyBackup?.flush(log); } } diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 2de71491..e9a8756d 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -19,6 +19,7 @@ import {StoredRoomKey, keyFromBackup} from "../decryption/RoomKey"; import {MEGOLM_ALGORITHM} from "../../common"; import * as Curve25519 from "./Curve25519"; import {AbortableOperation} from "../../../../utils/AbortableOperation"; +import {ObservableValue} from "../../../../observable/ObservableValue"; import {SetAbortableFn} from "../../../../utils/AbortableOperation"; import type {BackupInfo, SessionData, SessionKeyInfo, SessionInfo, KeyBackupPayload} from "./types"; @@ -33,7 +34,12 @@ import type {Transaction} from "../../../storage/idb/Transaction"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; +const KEYS_PER_REQUEST = 20; + export class KeyBackup { + public readonly operationInProgress = new ObservableValue, Progress> | undefined>(undefined); + public readonly needsNewKey = new ObservableValue(false); + constructor( private readonly backupInfo: BackupInfo, private readonly crypto: Curve25519.BackupEncryption, @@ -56,13 +62,32 @@ export class KeyBackup { } } - // TODO: protect against having multiple concurrent flushes - flush(log: ILogItem): AbortableOperation, Progress> { + flush(log: ILogItem): void { + if (!this.operationInProgress.get()) { + log.wrapDetached("flush key backup", async log => { + const operation = this._flush(log); + this.operationInProgress.set(operation); + try { + const success = await operation.result; + // stop key backup if the version was changed + if (!success) { + this.needsNewKey.set(true); + } + } catch (err) { + log.catch(err); + } + this.operationInProgress.set(undefined); + }); + } + } + + private _flush(log: ILogItem): AbortableOperation, Progress> { return new AbortableOperation(async (setAbortable, setProgress) => { let total = 0; let amountFinished = 0; while (true) { - const timeout = this.platform.clock.createTimeout(this.platform.random() * 10000); + const waitMs = this.platform.random() * 10000; + const timeout = this.platform.clock.createTimeout(waitMs); setAbortable(timeout); await timeout.elapsed(); const txn = await this.storage.readTxn([StoreNames.inboundGroupSessions]); @@ -70,7 +95,7 @@ export class KeyBackup { // fetch total again on each iteration as while we are flushing, sync might be adding keys total = await txn.inboundGroupSessions.countNonBackedUpSessions(); setProgress(new Progress(total, amountFinished)); - const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(20)) + const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(KEYS_PER_REQUEST)) .map(entry => new StoredRoomKey(entry)); if (keysNeedingBackup.length === 0) { return true; @@ -114,7 +139,9 @@ export class KeyBackup { ]); setAbortable(txn); try { - await Promise.all(roomKeys.map(key => txn.inboundGroupSessions.markAsBackedUp(key.roomId, key.senderKey, key.sessionId))); + await Promise.all(roomKeys.map(key => { + return txn.inboundGroupSessions.markAsBackedUp(key.roomId, key.senderKey, key.sessionId); + })); } catch (err) { txn.abort(); throw err; From c47bdd5715f39305c9305b49cced9a9f561a1f5c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:14:11 +0100 Subject: [PATCH 027/160] flush key backup when creating a new room key --- 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 2e6ee254..80f57507 100644 --- a/src/matrix/e2ee/RoomEncryption.js +++ b/src/matrix/e2ee/RoomEncryption.js @@ -236,6 +236,7 @@ export class RoomEncryption { this._keySharePromise = (async () => { const roomKeyMessage = await this._megolmEncryption.ensureOutboundSession(this._room.id, this._encryptionParams); if (roomKeyMessage) { + this._keyBackup?.flush(log); await log.wrap("share key", log => this._shareNewRoomKey(roomKeyMessage, hsApi, log)); } })(); @@ -254,6 +255,7 @@ export class RoomEncryption { } const megolmResult = await log.wrap("megolm encrypt", () => this._megolmEncryption.encrypt(this._room.id, type, content, this._encryptionParams)); if (megolmResult.roomKeyMessage) { + this._keyBackup?.flush(log); await log.wrap("share key", log => this._shareNewRoomKey(megolmResult.roomKeyMessage, hsApi, log)); } return { From 7d3e3b992b0e15b1ce73585be0d0f6a553b84942 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:14:38 +0100 Subject: [PATCH 028/160] some more typing --- src/matrix/e2ee/megolm/decryption/RoomKey.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/decryption/RoomKey.ts b/src/matrix/e2ee/megolm/decryption/RoomKey.ts index f4812acc..b5f75224 100644 --- a/src/matrix/e2ee/megolm/decryption/RoomKey.ts +++ b/src/matrix/e2ee/megolm/decryption/RoomKey.ts @@ -60,7 +60,7 @@ export abstract class IncomingRoomKey extends RoomKey { async write(loader: KeyLoader, txn: Transaction): Promise { // we checked already and we had a better session in storage, so don't write - let pickledSession; + let pickledSession: string | undefined; if (this.isBetter === undefined) { // if this key wasn't used to decrypt any messages in the same sync, // we haven't checked if this is the best key yet, From eb134a6c47df35cc14026bcd304bd2610abcddfc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 13:18:03 +0100 Subject: [PATCH 029/160] only take into account non-backed up keys for counting --- src/matrix/storage/idb/stores/InboundGroupSessionStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index a2ccae95..4d3497a4 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -77,7 +77,7 @@ export class InboundGroupSessionStore { this._store.delete(range); } countNonBackedUpSessions(): Promise { - return this._store.index("byBackup").count(); + return this._store.index("byBackup").count(this._store.IDBKeyRange.only(BackupStatus.NotBackedUp)); } getFirstNonBackedUpSessions(amount: number): Promise { From 504f420293f67e72a4435d1ff314ba6f229493e1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 15:13:58 +0100 Subject: [PATCH 030/160] make keyBackup an observable and don't have separate needs-key flag --- src/matrix/Session.js | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 7209e523..ab815f29 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -75,8 +75,7 @@ export class Session { this._megolmDecryption = null; this._getSyncToken = () => this.syncToken; this._olmWorker = olmWorker; - this._keyBackup = null; - this._hasSecretStorageKey = new ObservableValue(null); + this._keyBackup = new ObservableValue(undefined); this._observedRoomStatus = new Map(); if (olm) { @@ -171,10 +170,10 @@ export class Session { megolmEncryption: this._megolmEncryption, megolmDecryption: this._megolmDecryption, storage: this._storage, - keyBackup: this._keyBackup, + keyBackup: this._keyBackup?.get(), encryptionParams, notifyMissingMegolmSession: () => { - if (!this._keyBackup) { + if (!this._keyBackup.get()) { this.needsKeyBackup.set(true) } }, @@ -195,7 +194,7 @@ export class Session { if (!this._olm) { throw new Error("olm required"); } - if (this._keyBackup) { + if (this._keyBackup.get()) { return false; } const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); @@ -205,7 +204,6 @@ export class Session { ]); if (await this._createKeyBackup(key, readTxn, log)) { await this._writeSSSSKey(key); - this._hasSecretStorageKey.set(true); return key; } }); @@ -237,23 +235,22 @@ export class Session { throw err; } await writeTxn.complete(); - if (this._keyBackup) { + if (this._keyBackup.get()) { for (const room of this._rooms.values()) { if (room.isEncrypted) { room.enableKeyBackup(undefined); } } - this._keyBackup?.dispose(); - this._keyBackup = undefined; + this._keyBackup.get().dispose(); + this._keyBackup.set(undefined); } - this._hasSecretStorageKey.set(false); } _createKeyBackup(ssssKey, txn, log) { return log.wrap("enable key backup", async log => { try { const secretStorage = new SecretStorage({key: ssssKey, platform: this._platform}); - this._keyBackup = await KeyBackup.fromSecretStorage( + const keyBackup = await KeyBackup.fromSecretStorage( this._platform, this._olm, secretStorage, @@ -262,19 +259,18 @@ export class Session { this._storage, txn ); - if (this._keyBackup) { + if (keyBackup) { for (const room of this._rooms.values()) { if (room.isEncrypted) { - room.enableKeyBackup(this._keyBackup); + room.enableKeyBackup(keyBackup); } } + this._keyBackup.set(keyBackup); + return true; } - this.needsKeyBackup.set(false); } catch (err) { log.catch(err); - return false; } - return true; }); } @@ -417,8 +413,8 @@ export class Session { dispose() { this._olmWorker?.dispose(); this._olmWorker = undefined; - this._keyBackup?.dispose(); - this._keyBackup = undefined; + this._keyBackup.get()?.dispose(); + this._keyBackup.set(undefined); this._megolmDecryption?.dispose(); this._megolmDecryption = undefined; this._e2eeAccount?.dispose(); @@ -446,7 +442,7 @@ export class Session { await txn.complete(); } // enable session backup, this requests the latest backup version - if (!this._keyBackup) { + if (!this._keyBackup.get()) { if (dehydratedDevice) { await log.wrap("SSSSKeyFromDehydratedDeviceKey", async log => { const ssssKey = await createSSSSKeyFromDehydratedDeviceKey(dehydratedDevice.key, this._storage, this._platform); @@ -462,12 +458,10 @@ export class Session { ]); // try set up session backup if we stored the ssss key const ssssKey = await ssssReadKey(txn); - let couldReadKeyBackup = false; if (ssssKey) { // txn will end here as this does a network request - couldReadKeyBackup = await this._createKeyBackup(ssssKey, txn, log); + await this._createKeyBackup(ssssKey, txn, log); } - this._hasSecretStorageKey.set(couldReadKeyBackup); } // restore unfinished operations, like sending out room keys const opsTxn = await this._storage.readWriteTxn([ @@ -632,7 +626,7 @@ export class Session { } } if (changes.hasNewRoomKeys) { - this._keyBackup?.flush(log); + this._keyBackup.get()?.flush(log); } } From bd2c70b92326e6444cebfa69d1a09f2e9227ac4e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 15:14:23 +0100 Subject: [PATCH 031/160] adapt key backup view(model) to changes in session, show backup progress --- .../session/settings/KeyBackupViewModel.js | 79 ++++++++++++++++--- .../session/settings/KeyBackupSettingsView.js | 36 ++++++--- 2 files changed, 95 insertions(+), 20 deletions(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index 38d37c18..231fb4fe 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -18,7 +18,7 @@ import {ViewModel} from "../../ViewModel.js"; import {KeyType} from "../../../matrix/ssss/index"; import {createEnum} from "../../../utils/enum"; -export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending"); +export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending", "NewVersionAvailable"); export class KeyBackupViewModel extends ViewModel { constructor(options) { @@ -28,8 +28,11 @@ export class KeyBackupViewModel extends ViewModel { this._isBusy = false; this._dehydratedDeviceId = undefined; this._status = undefined; + this._needsNewKeySubscription = undefined; + this._operationSubscription = undefined; + this._operationProgressSubscription = undefined; this._reevaluateStatus(); - this.track(this._session.hasSecretStorageKey.subscribe(() => { + this.track(this._session.keyBackup.subscribe(() => { if (this._reevaluateStatus()) { this.emitChange("status"); } @@ -41,14 +44,31 @@ export class KeyBackupViewModel extends ViewModel { return false; } let status; - const hasSecretStorageKey = this._session.hasSecretStorageKey.get(); - if (hasSecretStorageKey === true) { - status = this._session.keyBackup ? Status.Enabled : Status.SetupKey; - } else if (hasSecretStorageKey === false) { - status = Status.SetupKey; + const keyBackup = this._session.keyBackup.get(); + if (keyBackup) { + if (!this._needsNewKeySubscription) { + this._needsNewKeySubscription = this.track(keyBackup.needsNewKey.subscribe(() => this._reevaluateStatus())); + } + if (!this._operationSubscription) { + this._operationSubscription = this.track(keyBackup.operationInProgress.subscribe(op => { + if (op && !this._operationProgressSubscription) { + this._operationProgressSubscription = this.track(op.progress.subscribe(() => this.emitChange("backupPercentage"))); + } else if (!op && this._operationProgressSubscription) { + this._operationProgressSubscription = this.disposeTracked(this._operationProgressSubscription); + } + this.emitChange("isBackingUp"); + })); + } + status = keyBackup.needsNewKey.get() ? Status.NewVersionAvailable : Status.Enabled; } else { + this._needsNewKeySubscription = this.disposeTracked(this._needsNewKeySubscription); + this._operationSubscription = this.disposeTracked(this._operationSubscription); + this._operationProgressSubscription = this.disposeTracked(this._operationProgressSubscription); + status = this.showPhraseSetup() ? Status.SetupPhrase : Status.SetupKey; + } /* TODO: bring back "waiting to get online" + else { status = Status.Pending; - } + } */ const changed = status !== this._status; this._status = status; return changed; @@ -75,7 +95,7 @@ export class KeyBackupViewModel extends ViewModel { } get backupVersion() { - return this._session.keyBackup?.version; + return this._session.keyBackup.get()?.version; } get status() { @@ -144,4 +164,45 @@ export class KeyBackupViewModel extends ViewModel { this.emitChange(""); } } + + get isBackingUp() { + const keyBackup = this._session.keyBackup.get(); + if (keyBackup) { + return !!keyBackup.operationInProgress.get(); + } + return undefined; + } + + get backupPercentage() { + const keyBackup = this._session.keyBackup.get(); + if (keyBackup) { + const op = keyBackup.operationInProgress.get(); + const progress = op.progress.get(); + if (progress) { + return Math.round(progress.finished / progress.total) * 100; + } + } + return 0; + } + + get backupInProgressLabel() { + const keyBackup = this._session.keyBackup.get(); + if (keyBackup) { + const op = keyBackup.operationInProgress.get(); + if (op) { + const progress = op.progress.get(); + if (progress) { + return this.i18n`${progress.finished} of ${progress.total}`; + } else { + return this.i18n`…`; + } + } + } + return undefined; + } + + cancelBackup() { + this._session.keyBackup.get()?.operationInProgress.get()?.abort(); + } } + diff --git a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js index bf3fd296..0d5d27e0 100644 --- a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js @@ -14,19 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {TemplateView, InlineTemplateView} from "../../general/TemplateView"; -import {StaticView} from "../../general/StaticView.js"; +import {TemplateView} from "../../general/TemplateView"; export class KeyBackupSettingsView extends TemplateView { - render(t, vm) { - return t.mapView(vm => vm.status, status => { - switch (status) { - case "Enabled": return new InlineTemplateView(vm, renderEnabled) - case "SetupKey": return new InlineTemplateView(vm, renderEnableFromKey) - case "SetupPhrase": return new InlineTemplateView(vm, renderEnableFromPhrase) - case "Pending": return new StaticView(vm, t => t.p(vm.i18n`Waiting to go online…`)) - } - }); + render(t) { + return t.div([ + t.map(vm => vm.status, (status, t, vm) => { + switch (status) { + case "Enabled": return renderEnabled(t, vm); + case "SetupKey": return renderEnableFromKey(t, vm); + case "SetupPhrase": return renderEnableFromPhrase(t, vm); + case "NewVersionAvailable": return t.p(vm.i18n`A new backup version has been created. Disable key backup and enable it again with the new key.`); + case "Pending": return t.p(vm.i18n`Waiting to go online…`); + } + }), + t.map(vm => vm.isBackingUp, (backingUp, t, vm) => { + if (backingUp) { + const progress = t.progress({ + min: 0, + max: 100, + value: vm => vm.backupPercentage, + }); + return t.div([`Backup in progress `, progress, " ", vm => vm.backupInProgressLabel]); + } else { + return t.p("All keys are backed up."); + } + }) + ]); } } From eabd303c8e9fb06d1f14e80a84bfc4d0439dcbd2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 15:14:58 +0100 Subject: [PATCH 032/160] count on the index if we're using one, don't always take the store --- src/matrix/storage/idb/Store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts index 20e6f79a..c9df33b2 100644 --- a/src/matrix/storage/idb/Store.ts +++ b/src/matrix/storage/idb/Store.ts @@ -120,7 +120,7 @@ export class QueryTargetWrapper { count(keyRange?: IDBKeyRange): IDBRequest { try { - return this._qtStore.count(keyRange); + return this._qt.count(keyRange); } catch(err) { throw new IDBRequestAttemptError("count", this._qt, err, [keyRange]); } From c340746a87987c5b631949f792fc08e21de85c18 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 16:04:56 +0100 Subject: [PATCH 033/160] also remove text nodes when updating message body fixes #649 --- src/platform/web/ui/session/room/timeline/TextMessageView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index 510a676f..c0c0cfb0 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -42,7 +42,8 @@ export class TextMessageView extends BaseMessageView { } })); - const shouldRemove = (element) => element?.nodeType === Node.ELEMENT_NODE && element.className !== "ReplyPreviewView"; + // exclude comment nodes as they are used by t.map and friends for placeholders + const shouldRemove = (element) => element?.nodeType !== Node.COMMENT_NODE && element.className !== "ReplyPreviewView"; t.mapSideEffect(vm => vm.body, body => { while (shouldRemove(container.lastChild)) { From e0df003aba75f13eb1150ecebefe0eda57e0aaf0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 16:35:49 +0100 Subject: [PATCH 034/160] add flatMap operator on observable value --- src/observable/ObservableValue.ts | 89 +++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/observable/ObservableValue.ts b/src/observable/ObservableValue.ts index b3ffa6ee..15d931ff 100644 --- a/src/observable/ObservableValue.ts +++ b/src/observable/ObservableValue.ts @@ -16,6 +16,7 @@ limitations under the License. import {AbortError} from "../utils/error"; import {BaseObservable} from "./BaseObservable"; +import type {SubscriptionHandle} from "./BaseObservable"; // like an EventEmitter, but doesn't have an event type export abstract class BaseObservableValue extends BaseObservable<(value: T) => void> { @@ -34,6 +35,10 @@ export abstract class BaseObservableValue extends BaseObservable<(value: T) = return new WaitForHandle(this, predicate); } } + + flatMap(mapper: (value: T) => (BaseObservableValue | undefined)): BaseObservableValue { + return new FlatMapObservableValue(this, mapper); + } } interface IWaitHandle { @@ -114,6 +119,61 @@ export class RetainedObservableValue extends ObservableValue { } } +export class FlatMapObservableValue extends BaseObservableValue { + private sourceSubscription?: SubscriptionHandle; + private targetSubscription?: SubscriptionHandle; + + constructor( + private readonly source: BaseObservableValue

, + private readonly mapper: (value: P) => (BaseObservableValue | undefined) + ) { + super(); + } + + onUnsubscribeLast() { + super.onUnsubscribeLast(); + this.sourceSubscription = this.sourceSubscription!(); + if (this.targetSubscription) { + this.targetSubscription = this.targetSubscription(); + } + } + + onSubscribeFirst() { + super.onSubscribeFirst(); + this.sourceSubscription = this.source.subscribe(() => { + this.updateTargetSubscription(); + this.emit(this.get()); + }); + this.updateTargetSubscription(); + } + + private updateTargetSubscription() { + const sourceValue = this.source.get(); + if (sourceValue) { + const target = this.mapper(sourceValue); + if (target) { + if (!this.targetSubscription) { + this.targetSubscription = target.subscribe(() => this.emit(this.get())); + } + return; + } + } + // if no sourceValue or target + if (this.targetSubscription) { + this.targetSubscription = this.targetSubscription(); + } + } + + get(): C | undefined { + const sourceValue = this.source.get(); + if (!sourceValue) { + return undefined; + } + const mapped = this.mapper(sourceValue); + return mapped?.get(); + } +} + export function tests() { return { "set emits an update": assert => { @@ -155,5 +215,34 @@ export function tests() { }); await assert.rejects(handle.promise, AbortError); }, + "flatMap.get": assert => { + const a = new ObservableValue}>(undefined); + const countProxy = a.flatMap(a => a.count); + assert.strictEqual(countProxy.get(), undefined); + const count = new ObservableValue(0); + a.set({count}); + assert.strictEqual(countProxy.get(), 0); + }, + "flatMap update from source": assert => { + const a = new ObservableValue}>(undefined); + const updates: (number | undefined)[] = []; + a.flatMap(a => a.count).subscribe(count => { + updates.push(count); + }); + const count = new ObservableValue(0); + a.set({count}); + assert.deepEqual(updates, [0]); + }, + "flatMap update from target": assert => { + const a = new ObservableValue}>(undefined); + const updates: (number | undefined)[] = []; + a.flatMap(a => a.count).subscribe(count => { + updates.push(count); + }); + const count = new ObservableValue(0); + a.set({count}); + count.set(5); + assert.deepEqual(updates, [0, 5]); + } } } From c9b5ce6508c5d664e425abf5a6665dda72caaafd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 16:36:13 +0100 Subject: [PATCH 035/160] clean up key backup vm using flatMap to avoid subscription handling --- .../session/settings/KeyBackupViewModel.js | 55 ++++--------------- 1 file changed, 12 insertions(+), 43 deletions(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index 231fb4fe..d88cd797 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -28,9 +28,10 @@ export class KeyBackupViewModel extends ViewModel { this._isBusy = false; this._dehydratedDeviceId = undefined; this._status = undefined; - this._needsNewKeySubscription = undefined; - this._operationSubscription = undefined; - this._operationProgressSubscription = undefined; + this._backupOperation = this._session.keyBackup.flatMap(keyBackup => keyBackup.operationInProgress); + this._progress = this._backupOperation.flatMap(op => op.progress); + this.track(this._backupOperation.subscribe(() => this.emitChange("isBackingUp"))); + this.track(this._progress.subscribe(() => this.emitChange("backupPercentage"))); this._reevaluateStatus(); this.track(this._session.keyBackup.subscribe(() => { if (this._reevaluateStatus()) { @@ -46,24 +47,8 @@ export class KeyBackupViewModel extends ViewModel { let status; const keyBackup = this._session.keyBackup.get(); if (keyBackup) { - if (!this._needsNewKeySubscription) { - this._needsNewKeySubscription = this.track(keyBackup.needsNewKey.subscribe(() => this._reevaluateStatus())); - } - if (!this._operationSubscription) { - this._operationSubscription = this.track(keyBackup.operationInProgress.subscribe(op => { - if (op && !this._operationProgressSubscription) { - this._operationProgressSubscription = this.track(op.progress.subscribe(() => this.emitChange("backupPercentage"))); - } else if (!op && this._operationProgressSubscription) { - this._operationProgressSubscription = this.disposeTracked(this._operationProgressSubscription); - } - this.emitChange("isBackingUp"); - })); - } status = keyBackup.needsNewKey.get() ? Status.NewVersionAvailable : Status.Enabled; } else { - this._needsNewKeySubscription = this.disposeTracked(this._needsNewKeySubscription); - this._operationSubscription = this.disposeTracked(this._operationSubscription); - this._operationProgressSubscription = this.disposeTracked(this._operationProgressSubscription); status = this.showPhraseSetup() ? Status.SetupPhrase : Status.SetupKey; } /* TODO: bring back "waiting to get online" else { @@ -166,39 +151,23 @@ export class KeyBackupViewModel extends ViewModel { } get isBackingUp() { - const keyBackup = this._session.keyBackup.get(); - if (keyBackup) { - return !!keyBackup.operationInProgress.get(); - } - return undefined; + return !!this._backupOperation.get(); } get backupPercentage() { - const keyBackup = this._session.keyBackup.get(); - if (keyBackup) { - const op = keyBackup.operationInProgress.get(); - const progress = op.progress.get(); - if (progress) { - return Math.round(progress.finished / progress.total) * 100; - } + const progress = this._progress.get(); + if (progress) { + return Math.round(progress.finished / progress.total) * 100; } return 0; } get backupInProgressLabel() { - const keyBackup = this._session.keyBackup.get(); - if (keyBackup) { - const op = keyBackup.operationInProgress.get(); - if (op) { - const progress = op.progress.get(); - if (progress) { - return this.i18n`${progress.finished} of ${progress.total}`; - } else { - return this.i18n`…`; - } - } + const progress = this._progress.get(); + if (progress) { + return this.i18n`${progress.finished} of ${progress.total}`; } - return undefined; + return this.i18n`…`; } cancelBackup() { From a97d235cf56092f819e1a08b307dbac07f4d62b1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 16:36:42 +0100 Subject: [PATCH 036/160] flush after enabling key backup --- src/matrix/Session.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index ab815f29..3d1530c9 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -204,6 +204,7 @@ export class Session { ]); if (await this._createKeyBackup(key, readTxn, log)) { await this._writeSSSSKey(key); + this._keyBackup.get().flush(log); return key; } }); From d9e6164a5cd06542e07a76c2a6961a0eba95600c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Jan 2022 16:40:32 +0100 Subject: [PATCH 037/160] fix ts errors --- src/observable/ObservableValue.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/observable/ObservableValue.ts b/src/observable/ObservableValue.ts index 15d931ff..ad0a226d 100644 --- a/src/observable/ObservableValue.ts +++ b/src/observable/ObservableValue.ts @@ -217,7 +217,7 @@ export function tests() { }, "flatMap.get": assert => { const a = new ObservableValue}>(undefined); - const countProxy = a.flatMap(a => a.count); + const countProxy = a.flatMap(a => a!.count); assert.strictEqual(countProxy.get(), undefined); const count = new ObservableValue(0); a.set({count}); @@ -226,7 +226,7 @@ export function tests() { "flatMap update from source": assert => { const a = new ObservableValue}>(undefined); const updates: (number | undefined)[] = []; - a.flatMap(a => a.count).subscribe(count => { + a.flatMap(a => a!.count).subscribe(count => { updates.push(count); }); const count = new ObservableValue(0); @@ -236,7 +236,7 @@ export function tests() { "flatMap update from target": assert => { const a = new ObservableValue}>(undefined); const updates: (number | undefined)[] = []; - a.flatMap(a => a.count).subscribe(count => { + a.flatMap(a => a!.count).subscribe(count => { updates.push(count); }); const count = new ObservableValue(0); From ae5cc172907a6b6e39f7e4b9267521e2bec6c2e9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 14:36:04 +0100 Subject: [PATCH 038/160] mark all inbound sessions to be backed up again when changing version --- src/matrix/Session.js | 21 ++++++++++++++----- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 4 ++++ src/matrix/ssss/index.ts | 6 +++++- .../idb/stores/InboundGroupSessionStore.ts | 12 +++++++++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 3d1530c9..2a1618a2 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -203,21 +203,32 @@ export class Session { this._storage.storeNames.accountData, ]); if (await this._createKeyBackup(key, readTxn, log)) { - await this._writeSSSSKey(key); + // only after having read a secret, write the key + // as we only find out if it was good if the MAC verification succeeds + await this._writeSSSSKey(key, log); this._keyBackup.get().flush(log); return key; } }); } - async _writeSSSSKey(key) { - // only after having read a secret, write the key - // as we only find out if it was good if the MAC verification succeeds + async _writeSSSSKey(key, log) { + // we're going to write the 4S key, and also the backup version. + // this way, we can detect when we enter a key for a new backup version + // and mark all inbound sessions to be backed up again + const backupVersion = this._keyBackup.get()?.version; const writeTxn = await this._storage.readWriteTxn([ this._storage.storeNames.session, + this._storage.storeNames.inboundGroupSessions, ]); try { - ssssWriteKey(key, writeTxn); + const previousBackupVersion = await ssssWriteKey(key, backupVersion, writeTxn); + log.set("previousBackupVersion", previousBackupVersion); + log.set("backupVersion", backupVersion); + if (typeof previousBackupVersion === "number" && previousBackupVersion !== backupVersion) { + const amountMarked = await this._keyBackup.markAllForBackup(writeTxn); + log.set("amountMarkedForBackup", amountMarked); + } } catch (err) { writeTxn.abort(); throw err; diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index e9a8756d..8f9611dd 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -62,6 +62,10 @@ export class KeyBackup { } } + markAllForBackup(txn: Transaction): Promise { + return txn.inboundGroupSessions.markAllAsNotBackedUp(); + } + flush(log: ILogItem): void { if (!this.operationInProgress.get()) { log.wrapDetached("flush key backup", async log => { diff --git a/src/matrix/ssss/index.ts b/src/matrix/ssss/index.ts index 37f47963..fd4c2245 100644 --- a/src/matrix/ssss/index.ts +++ b/src/matrix/ssss/index.ts @@ -27,6 +27,7 @@ import type * as OlmNamespace from "@matrix-org/olm" type Olm = typeof OlmNamespace; const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`; +const BACKUPVERSION_KEY = `${SESSION_E2EE_KEY_PREFIX}keyBackupVersion`; export enum KeyType { "RecoveryKey", @@ -49,8 +50,11 @@ async function readDefaultKeyDescription(storage: Storage): Promise { +export async function writeKey(key: Key, keyBackupVersion: number, txn: Transaction): Promise { + const existingVersion: number | undefined = await txn.session.get(BACKUPVERSION_KEY); + txn.session.set(BACKUPVERSION_KEY, keyBackupVersion); txn.session.set(SSSS_KEY, {id: key.id, binaryKey: key.binaryKey}); + return existingVersion; } export async function readKey(txn: Transaction): Promise { diff --git a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts index 4d3497a4..b78c817e 100644 --- a/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts +++ b/src/matrix/storage/idb/stores/InboundGroupSessionStore.ts @@ -91,4 +91,16 @@ export class InboundGroupSessionStore { this._store.put(entry); } } + + async markAllAsNotBackedUp(): Promise { + const backedUpKey = this._store.IDBKeyRange.only(BackupStatus.BackedUp); + let count = 0; + await this._store.index("byBackup").iterateValues(backedUpKey, (val: InboundGroupSessionEntry, key: IDBValidKey, cur: IDBCursorWithValue) => { + val.backup = BackupStatus.NotBackedUp; + cur.update(val); + count += 1; + return false; + }); + return count; + } } From 7eb0d347f5b000736d4c3b18b9e16c760d0d4146 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 14:36:35 +0100 Subject: [PATCH 039/160] flush key backup after coming online --- src/matrix/Session.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 2a1618a2..a5270476 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -473,6 +473,7 @@ export class Session { if (ssssKey) { // txn will end here as this does a network request await this._createKeyBackup(ssssKey, txn, log); + this._keyBackup.get()?.flush(); } } // restore unfinished operations, like sending out room keys From a757fb3696029f011a016cb613e70c0a56bfa3f4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 14:37:05 +0100 Subject: [PATCH 040/160] better error handling in key backup, cleanup and not overuse observables --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 8f9611dd..c693506d 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -37,8 +37,11 @@ type Olm = typeof OlmNamespace; const KEYS_PER_REQUEST = 20; export class KeyBackup { - public readonly operationInProgress = new ObservableValue, Progress> | undefined>(undefined); - public readonly needsNewKey = new ObservableValue(false); + public readonly operationInProgress = new ObservableValue, Progress> | undefined>(undefined); + + private _cancelled = false; + private _needsNewKey = false; + private _error?: Error; constructor( private readonly backupInfo: BackupInfo, @@ -49,6 +52,10 @@ export class KeyBackup { private readonly platform: Platform, ) {} + get cancelled(): boolean { return this._cancelled; } + get needsNewKey(): boolean { return this._needsNewKey; } + get error(): Error | undefined { return this._error; } + async getRoomKey(roomId: string, sessionId: string, log: ILogItem): Promise { const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response(); if (!sessionResponse.session_data) { @@ -69,15 +76,27 @@ export class KeyBackup { flush(log: ILogItem): void { if (!this.operationInProgress.get()) { log.wrapDetached("flush key backup", async log => { - const operation = this._flush(log); + if (this._needsNewKey) { + log.set("needsNewKey", this._needsNewKey); + return; + } + this._cancelled = false; + this._error = undefined; + const operation = this._runFlushOperation(log); this.operationInProgress.set(operation); try { - const success = await operation.result; - // stop key backup if the version was changed - if (!success) { - this.needsNewKey.set(true); - } + await operation.result; } catch (err) { + if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { + log.set("wrong_version", true); + this._needsNewKey = true; + } else { + this._cancelled = true; + // TODO should really also use AbortError in storage + if (err.name !== "AbortError" || (err.name === "StorageError" && err.errcode === "AbortError")) { + this._error = err; + } + } log.catch(err); } this.operationInProgress.set(undefined); @@ -85,7 +104,7 @@ export class KeyBackup { } } - private _flush(log: ILogItem): AbortableOperation, Progress> { + private _runFlushOperation(log: ILogItem): AbortableOperation, Progress> { return new AbortableOperation(async (setAbortable, setProgress) => { let total = 0; let amountFinished = 0; @@ -102,21 +121,12 @@ export class KeyBackup { const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(KEYS_PER_REQUEST)) .map(entry => new StoredRoomKey(entry)); if (keysNeedingBackup.length === 0) { - return true; + return; } const payload = await this.encodeKeysForBackup(keysNeedingBackup); const uploadRequest = this.hsApi.uploadRoomKeysToBackup(this.backupInfo.version, payload, {log}); setAbortable(uploadRequest); - try { - await uploadRequest.response(); - } catch (err) { - if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { - log.set("wrong_version", true); - return false; - } else { - throw err; - } - } + await uploadRequest.response(); await this.markKeysAsBackedUp(keysNeedingBackup, setAbortable); amountFinished += keysNeedingBackup.length; setProgress(new Progress(total, amountFinished)); From dacaa86386e4ac64321deec567b09a34f4feeaa9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 16:22:22 +0100 Subject: [PATCH 041/160] fix percentage calculation --- src/domain/session/settings/KeyBackupViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index d88cd797..391aa09e 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -157,7 +157,7 @@ export class KeyBackupViewModel extends ViewModel { get backupPercentage() { const progress = this._progress.get(); if (progress) { - return Math.round(progress.finished / progress.total) * 100; + return Math.round((progress.finished / progress.total) * 100); } return 0; } From 6541aacf9877a63d37d3ffa3528998e0a27f9200 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 16:23:48 +0100 Subject: [PATCH 042/160] don't discount already finished keys in total for previous iterations --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index c693506d..de4a978b 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -116,7 +116,7 @@ export class KeyBackup { const txn = await this.storage.readTxn([StoreNames.inboundGroupSessions]); setAbortable(txn); // fetch total again on each iteration as while we are flushing, sync might be adding keys - total = await txn.inboundGroupSessions.countNonBackedUpSessions(); + total = amountFinished + await txn.inboundGroupSessions.countNonBackedUpSessions(); setProgress(new Progress(total, amountFinished)); const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(KEYS_PER_REQUEST)) .map(entry => new StoredRoomKey(entry)); From 06a1421e9769e94406f284d90765624cbffc987d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 16:25:08 +0100 Subject: [PATCH 043/160] add backupWriteStatus so binding can take multiple fields into account --- .../session/settings/KeyBackupViewModel.js | 38 +++++++++++++-- .../session/settings/KeyBackupSettingsView.js | 47 ++++++++++++++----- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index 391aa09e..133139ed 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -18,7 +18,8 @@ import {ViewModel} from "../../ViewModel.js"; import {KeyType} from "../../../matrix/ssss/index"; import {createEnum} from "../../../utils/enum"; -export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending", "NewVersionAvailable"); +export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending", "NewVersionAvailable", "Error", "Cancelled"); +export const BackupWriteStatus = createEnum("Writing", "Stopped", "Done", "Pending"); export class KeyBackupViewModel extends ViewModel { constructor(options) { @@ -30,7 +31,11 @@ export class KeyBackupViewModel extends ViewModel { this._status = undefined; this._backupOperation = this._session.keyBackup.flatMap(keyBackup => keyBackup.operationInProgress); this._progress = this._backupOperation.flatMap(op => op.progress); - this.track(this._backupOperation.subscribe(() => this.emitChange("isBackingUp"))); + this.track(this._backupOperation.subscribe(() => { + // see if needsNewKey might be set + this._reevaluateStatus(); + this.emitChange("isBackingUp"); + })); this.track(this._progress.subscribe(() => this.emitChange("backupPercentage"))); this._reevaluateStatus(); this.track(this._session.keyBackup.subscribe(() => { @@ -47,7 +52,7 @@ export class KeyBackupViewModel extends ViewModel { let status; const keyBackup = this._session.keyBackup.get(); if (keyBackup) { - status = keyBackup.needsNewKey.get() ? Status.NewVersionAvailable : Status.Enabled; + status = keyBackup.needsNewKey ? Status.NewVersionAvailable : Status.Enabled; } else { status = this.showPhraseSetup() ? Status.SetupPhrase : Status.SetupKey; } /* TODO: bring back "waiting to get online" @@ -83,6 +88,27 @@ export class KeyBackupViewModel extends ViewModel { return this._session.keyBackup.get()?.version; } + get backupWriteStatus() { + const keyBackup = this._session.keyBackup.get(); + if (!keyBackup) { + return BackupWriteStatus.Pending; + } else if (keyBackup.hasStopped) { + return BackupWriteStatus.Stopped; + } + const operation = keyBackup.operationInProgress.get(); + if (operation) { + return BackupWriteStatus.Writing; + } else if (keyBackup.hasBackedUpAllKeys) { + return BackupWriteStatus.Done; + } else { + return BackupWriteStatus.Pending; + } + } + + get backupError() { + return this._session.keyBackup.get()?.error?.message; + } + get status() { return this._status; } @@ -171,7 +197,11 @@ export class KeyBackupViewModel extends ViewModel { } cancelBackup() { - this._session.keyBackup.get()?.operationInProgress.get()?.abort(); + this._backupOperation.get()?.abort(); + } + + startBackup() { + this._session.keyBackup.get()?.flush(); } } diff --git a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js index 0d5d27e0..3f8812c9 100644 --- a/src/platform/web/ui/session/settings/KeyBackupSettingsView.js +++ b/src/platform/web/ui/session/settings/KeyBackupSettingsView.js @@ -22,22 +22,36 @@ export class KeyBackupSettingsView extends TemplateView { t.map(vm => vm.status, (status, t, vm) => { switch (status) { case "Enabled": return renderEnabled(t, vm); + case "NewVersionAvailable": return renderNewVersionAvailable(t, vm); case "SetupKey": return renderEnableFromKey(t, vm); case "SetupPhrase": return renderEnableFromPhrase(t, vm); - case "NewVersionAvailable": return t.p(vm.i18n`A new backup version has been created. Disable key backup and enable it again with the new key.`); case "Pending": return t.p(vm.i18n`Waiting to go online…`); } }), - t.map(vm => vm.isBackingUp, (backingUp, t, vm) => { - if (backingUp) { - const progress = t.progress({ - min: 0, - max: 100, - value: vm => vm.backupPercentage, - }); - return t.div([`Backup in progress `, progress, " ", vm => vm.backupInProgressLabel]); - } else { - return t.p("All keys are backed up."); + t.map(vm => vm.backupWriteStatus, (status, t, vm) => { + switch (status) { + case "Writing": { + const progress = t.progress({ + min: 0, + max: 100, + value: vm => vm.backupPercentage, + }); + return t.div([`Backup in progress `, progress, " ", vm => vm.backupInProgressLabel]); + } + case "Stopped": { + let label; + const error = vm.backupError; + if (error) { + label = `Backup has stopped because of an error: ${vm.backupError}`; + } else { + label = `Backup has stopped`; + } + return t.p(label, " ", t.button({onClick: () => vm.startBackup()}, `Backup now`)); + } + case "Done": + return t.p(`All keys are backed up.`); + default: + return null; } }) ]); @@ -46,7 +60,7 @@ export class KeyBackupSettingsView extends TemplateView { function renderEnabled(t, vm) { const items = [ - t.p([vm.i18n`Session backup is enabled, using backup version ${vm.backupVersion}. `, t.button({onClick: () => vm.disable()}, vm.i18n`Disable`)]) + t.p([vm.i18n`Key backup is enabled, using backup version ${vm.backupVersion}. `, t.button({onClick: () => vm.disable()}, vm.i18n`Disable`)]) ]; if (vm.dehydratedDeviceId) { items.push(t.p(vm.i18n`A dehydrated device id was set up with id ${vm.dehydratedDeviceId} which you can use during your next login with your secret storage key.`)); @@ -54,6 +68,13 @@ function renderEnabled(t, vm) { return t.div(items); } +function renderNewVersionAvailable(t, vm) { + const items = [ + t.p([vm.i18n`A new backup version has been created from another device. Disable key backup and enable it again with the new key.`, t.button({onClick: () => vm.disable()}, vm.i18n`Disable`)]) + ]; + return t.div(items); +} + function renderEnableFromKey(t, vm) { const useASecurityPhrase = t.button({className: "link", onClick: () => vm.showPhraseSetup()}, vm.i18n`use a security phrase`); return t.div([ @@ -101,7 +122,7 @@ function renderEnableFieldRow(t, vm, label, callback) { function renderError(t) { return t.if(vm => vm.error, (t, vm) => { return t.div([ - t.p({className: "error"}, vm => vm.i18n`Could not enable session backup: ${vm.error}.`), + t.p({className: "error"}, vm => vm.i18n`Could not enable key backup: ${vm.error}.`), t.p(vm.i18n`Try double checking that you did not mix up your security key, security phrase and login password as explained above.`) ]) }); From 830786b2fdb31c17cbd1dbf8d2bbb994f29884ae Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 16:26:14 +0100 Subject: [PATCH 044/160] fixes and cleanup --- src/matrix/Session.js | 14 ++++++++++---- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 19 ++++++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index a5270476..17c809bd 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -208,6 +208,8 @@ export class Session { await this._writeSSSSKey(key, log); this._keyBackup.get().flush(log); return key; + } else { + throw new Error("Could not read key backup with the given key"); } }); } @@ -216,7 +218,11 @@ export class Session { // we're going to write the 4S key, and also the backup version. // this way, we can detect when we enter a key for a new backup version // and mark all inbound sessions to be backed up again - const backupVersion = this._keyBackup.get()?.version; + const keyBackup = this._keyBackup.get(); + if (!keyBackup) { + return; + } + const backupVersion = keyBackup.version; const writeTxn = await this._storage.readWriteTxn([ this._storage.storeNames.session, this._storage.storeNames.inboundGroupSessions, @@ -225,8 +231,8 @@ export class Session { const previousBackupVersion = await ssssWriteKey(key, backupVersion, writeTxn); log.set("previousBackupVersion", previousBackupVersion); log.set("backupVersion", backupVersion); - if (typeof previousBackupVersion === "number" && previousBackupVersion !== backupVersion) { - const amountMarked = await this._keyBackup.markAllForBackup(writeTxn); + if (!!previousBackupVersion && previousBackupVersion !== backupVersion) { + const amountMarked = await keyBackup.markAllForBackup(writeTxn); log.set("amountMarkedForBackup", amountMarked); } } catch (err) { @@ -473,7 +479,7 @@ export class Session { if (ssssKey) { // txn will end here as this does a network request await this._createKeyBackup(ssssKey, txn, log); - this._keyBackup.get()?.flush(); + this._keyBackup.get()?.flush(log); } } // restore unfinished operations, like sending out room keys diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index de4a978b..e20ccc1a 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -39,8 +39,9 @@ const KEYS_PER_REQUEST = 20; export class KeyBackup { public readonly operationInProgress = new ObservableValue, Progress> | undefined>(undefined); - private _cancelled = false; + private _stopped = false; private _needsNewKey = false; + private _hasBackedUpAllKeys = false; private _error?: Error; constructor( @@ -52,9 +53,11 @@ export class KeyBackup { private readonly platform: Platform, ) {} - get cancelled(): boolean { return this._cancelled; } - get needsNewKey(): boolean { return this._needsNewKey; } + get hasStopped(): boolean { return this._stopped; } get error(): Error | undefined { return this._error; } + get version(): string { return this.backupInfo.version; } + get needsNewKey(): boolean { return this._needsNewKey; } + get hasBackedUpAllKeys(): boolean { return this._hasBackedUpAllKeys; } async getRoomKey(roomId: string, sessionId: string, log: ILogItem): Promise { const sessionResponse = await this.hsApi.roomKeyForRoomAndSession(this.backupInfo.version, roomId, sessionId, {log}).response(); @@ -80,18 +83,20 @@ export class KeyBackup { log.set("needsNewKey", this._needsNewKey); return; } - this._cancelled = false; + this._stopped = false; this._error = undefined; + this._hasBackedUpAllKeys = false; const operation = this._runFlushOperation(log); this.operationInProgress.set(operation); try { await operation.result; + this._hasBackedUpAllKeys = true; } catch (err) { + this._stopped = true; if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { log.set("wrong_version", true); this._needsNewKey = true; } else { - this._cancelled = true; // TODO should really also use AbortError in storage if (err.name !== "AbortError" || (err.name === "StorageError" && err.errcode === "AbortError")) { this._error = err; @@ -176,10 +181,6 @@ export class KeyBackup { }); } - get version(): string { - return this.backupInfo.version; - } - dispose() { this.crypto.dispose(); } From 17275a53903a2592fe0215002f4f22b9dad8b897 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 17:30:15 +0100 Subject: [PATCH 045/160] backup 200 keys per request --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index e20ccc1a..998fb67f 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -34,7 +34,7 @@ import type {Transaction} from "../../../storage/idb/Transaction"; import type * as OlmNamespace from "@matrix-org/olm"; type Olm = typeof OlmNamespace; -const KEYS_PER_REQUEST = 20; +const KEYS_PER_REQUEST = 200; export class KeyBackup { public readonly operationInProgress = new ObservableValue, Progress> | undefined>(undefined); From 62acd458c6fbf66499ab25ca594975adf3cb1a2c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 17:30:51 +0100 Subject: [PATCH 046/160] also ask for new key if backup version is not found --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 998fb67f..e97eb0cf 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -93,7 +93,7 @@ export class KeyBackup { this._hasBackedUpAllKeys = true; } catch (err) { this._stopped = true; - if (err.name === "HomeServerError" && err.errcode === "M_WRONG_ROOM_KEYS_VERSION") { + if (err.name === "HomeServerError" && (err.errcode === "M_WRONG_ROOM_KEYS_VERSION" || err.errcode === "M_NOT_FOUND")) { log.set("wrong_version", true); this._needsNewKey = true; } else { From 9c599d53aa74922fb3821b7ef2ecf6bf601ff1c1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 17:31:01 +0100 Subject: [PATCH 047/160] allow to inject max delay in key backup --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index e97eb0cf..089d2b0a 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -51,6 +51,7 @@ export class KeyBackup { private readonly keyLoader: KeyLoader, private readonly storage: Storage, private readonly platform: Platform, + private readonly maxDelay: number = 10000 ) {} get hasStopped(): boolean { return this._stopped; } @@ -114,7 +115,7 @@ export class KeyBackup { let total = 0; let amountFinished = 0; while (true) { - const waitMs = this.platform.random() * 10000; + const waitMs = this.platform.random() * this.maxDelay; const timeout = this.platform.clock.createTimeout(waitMs); setAbortable(timeout); await timeout.elapsed(); From 997666164c974843ed9da8fa43a6a3d2a92cafb1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 31 Jan 2022 17:37:44 +0100 Subject: [PATCH 048/160] remove unused enum variants --- src/domain/session/settings/KeyBackupViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index 133139ed..69e87e5d 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -18,7 +18,7 @@ import {ViewModel} from "../../ViewModel.js"; import {KeyType} from "../../../matrix/ssss/index"; import {createEnum} from "../../../utils/enum"; -export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending", "NewVersionAvailable", "Error", "Cancelled"); +export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending", "NewVersionAvailable"); export const BackupWriteStatus = createEnum("Writing", "Stopped", "Done", "Pending"); export class KeyBackupViewModel extends ViewModel { From fd4eb6b50da969ed01e73735c5418c0cd11f9fb5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Feb 2022 11:08:13 +0100 Subject: [PATCH 049/160] distinguish between "waiting to go online" vs "backup not configured" --- .../session/settings/KeyBackupViewModel.js | 7 +++---- src/matrix/Session.js | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/domain/session/settings/KeyBackupViewModel.js b/src/domain/session/settings/KeyBackupViewModel.js index 69e87e5d..b44de7e5 100644 --- a/src/domain/session/settings/KeyBackupViewModel.js +++ b/src/domain/session/settings/KeyBackupViewModel.js @@ -53,12 +53,11 @@ export class KeyBackupViewModel extends ViewModel { const keyBackup = this._session.keyBackup.get(); if (keyBackup) { status = keyBackup.needsNewKey ? Status.NewVersionAvailable : Status.Enabled; - } else { + } else if (keyBackup === null) { status = this.showPhraseSetup() ? Status.SetupPhrase : Status.SetupKey; - } /* TODO: bring back "waiting to get online" - else { + } else { status = Status.Pending; - } */ + } const changed = status !== this._status; this._status = status; return changed; diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 17c809bd..868a743b 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -289,9 +289,15 @@ export class Session { } catch (err) { log.catch(err); } + return false; }); } + /** + * @type {ObservableValue Date: Tue, 1 Feb 2022 11:26:00 +0100 Subject: [PATCH 050/160] don't block reenabling 4s if already enabled --- src/matrix/Session.js | 3 ++- src/matrix/e2ee/megolm/keybackup/Curve25519.ts | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 868a743b..3404a6aa 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -195,7 +195,8 @@ export class Session { throw new Error("olm required"); } if (this._keyBackup.get()) { - return false; + this._keyBackup.get().dispose(); + this._keyBackup.set(undefined); } const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); // and create key backup, which needs to read from accountData diff --git a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts index 8007d422..7d2ebac7 100644 --- a/src/matrix/e2ee/megolm/keybackup/Curve25519.ts +++ b/src/matrix/e2ee/megolm/keybackup/Curve25519.ts @@ -41,8 +41,8 @@ export type SessionData = { export class BackupEncryption { constructor( - private readonly encryption: Olm.PkEncryption, - private readonly decryption: Olm.PkDecryption + private encryption?: Olm.PkEncryption, + private decryption?: Olm.PkDecryption ) {} static fromAuthData(authData: AuthData, privateKey: Uint8Array, olm: Olm): BackupEncryption { @@ -63,7 +63,7 @@ export class BackupEncryption { } decryptRoomKey(sessionData: SessionData): SessionKeyInfo { - const sessionInfo = this.decryption.decrypt( + const sessionInfo = this.decryption!.decrypt( sessionData.ephemeral, sessionData.mac, sessionData.ciphertext, @@ -79,11 +79,13 @@ export class BackupEncryption { forwarding_curve25519_key_chain: [], session_key: sessionKey }; - return this.encryption.encrypt(JSON.stringify(sessionInfo)) as SessionData; + return this.encryption!.encrypt(JSON.stringify(sessionInfo)) as SessionData; } dispose() { - this.decryption.free(); - this.encryption.free(); + this.decryption?.free(); + this.decryption = undefined; + this.encryption?.free(); + this.encryption = undefined; } } From f4bb420f35fa31e408311fa7fa3b7a0ba61f454b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Feb 2022 11:27:42 +0100 Subject: [PATCH 051/160] mark key backup properly as disabled --- src/matrix/Session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 3404a6aa..668677b3 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -261,7 +261,7 @@ export class Session { } } this._keyBackup.get().dispose(); - this._keyBackup.set(undefined); + this._keyBackup.set(null); } } From f4fa013ebc7ded486c683fe8431dc2dc1a632079 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Feb 2022 11:32:53 +0100 Subject: [PATCH 052/160] mark as not configured yet when re-enabling key backup --- src/matrix/Session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 668677b3..a2725bd4 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -196,7 +196,7 @@ export class Session { } if (this._keyBackup.get()) { this._keyBackup.get().dispose(); - this._keyBackup.set(undefined); + this._keyBackup.set(null); } const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform, this._olm); // and create key backup, which needs to read from accountData From c6820eccabd0469e1b170e0ff4453f677a988dd1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Feb 2022 11:58:47 +0100 Subject: [PATCH 053/160] release v0.2.24 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 21a731b5..913de9bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.23", + "version": "0.2.24", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From b29ecd339db63044962735637568183b4ba08349 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Feb 2022 12:18:28 +0100 Subject: [PATCH 054/160] add more logging to backup storage migration --- src/matrix/storage/idb/schema.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 9c105a71..fa4374cd 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -281,14 +281,23 @@ async function clearAllStores(db: IDBDatabase, txn: IDBTransaction) { // v15 add backup index to inboundGroupSessions async function addInboundSessionBackupIndex(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem): Promise { const inboundGroupSessions = txn.objectStore("inboundGroupSessions"); + let countWithSession = 0; + let countWithoutSession = 0; await iterateCursor(inboundGroupSessions.openCursor(), (value, key, cursor) => { - value.backup = BackupStatus.NotBackedUp; - // we'll also have backup keys in here, we can't tell, - // but the worst thing that can happen is that we try - // to backup keys that were already in backup, which - // the server will ignore - value.source = KeySource.DeviceMessage; + if (value.session) { + value.backup = BackupStatus.NotBackedUp; + // we'll also have backup keys in here, we can't tell, + // but the worst thing that can happen is that we try + // to backup keys that were already in backup, which + // the server will ignore + value.source = KeySource.DeviceMessage; + countWithSession += 1; + } else { + countWithoutSession += 1; + } return NOT_DONE; }); + log.set("countWithoutSession", countWithoutSession); + log.set("countWithSession", countWithSession); inboundGroupSessions.createIndex("byBackup", "backup", {unique: false}); } From 00c5e747d2021b93be2322279f48a503f6740112 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Feb 2022 12:30:45 +0100 Subject: [PATCH 055/160] log total backed up keys during flush operation --- src/matrix/e2ee/megolm/keybackup/KeyBackup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts index 089d2b0a..43631552 100644 --- a/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts +++ b/src/matrix/e2ee/megolm/keybackup/KeyBackup.ts @@ -127,6 +127,7 @@ export class KeyBackup { const keysNeedingBackup = (await txn.inboundGroupSessions.getFirstNonBackedUpSessions(KEYS_PER_REQUEST)) .map(entry => new StoredRoomKey(entry)); if (keysNeedingBackup.length === 0) { + log.set("total", total); return; } const payload = await this.encodeKeysForBackup(keysNeedingBackup); From 2e3616e05dc082bf65885caf7e5b0d5cb4cb23b6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Feb 2022 12:31:10 +0100 Subject: [PATCH 056/160] call cursor.update during backup field migration, needs new version --- src/matrix/storage/idb/schema.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index fa4374cd..7819130e 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -33,7 +33,8 @@ export const schema: MigrationFunc[] = [ changeSSSSKeyPrefix, backupAndRestoreE2EEAccountToLocalStorage, clearAllStores, - addInboundSessionBackupIndex + addInboundSessionBackupIndex, + migrateBackupStatus ]; // TODO: how to deal with git merge conflicts of this array? @@ -280,6 +281,13 @@ async function clearAllStores(db: IDBDatabase, txn: IDBTransaction) { // v15 add backup index to inboundGroupSessions async function addInboundSessionBackupIndex(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem): Promise { + const inboundGroupSessions = txn.objectStore("inboundGroupSessions"); + inboundGroupSessions.createIndex("byBackup", "backup", {unique: false}); +} + + +// v16 migrates the backup and source fields of inbound group sessions +async function migrateBackupStatus(db: IDBDatabase, txn: IDBTransaction, localStorage: IDOMStorage, log: ILogItem): Promise { const inboundGroupSessions = txn.objectStore("inboundGroupSessions"); let countWithSession = 0; let countWithoutSession = 0; @@ -291,6 +299,7 @@ async function addInboundSessionBackupIndex(db: IDBDatabase, txn: IDBTransaction // to backup keys that were already in backup, which // the server will ignore value.source = KeySource.DeviceMessage; + cursor.update(value); countWithSession += 1; } else { countWithoutSession += 1; @@ -299,5 +308,4 @@ async function addInboundSessionBackupIndex(db: IDBDatabase, txn: IDBTransaction }); log.set("countWithoutSession", countWithoutSession); log.set("countWithSession", countWithSession); - inboundGroupSessions.createIndex("byBackup", "backup", {unique: false}); } From 65dcf8bc360413a09be4d2b1ee48a051281b7109 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 1 Feb 2022 12:34:42 +0100 Subject: [PATCH 057/160] release v0.2.25 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 913de9bf..18122ed2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.24", + "version": "0.2.25", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From 348de312f9a69f1a8550cc50d1b94bdcf65edbf0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 2 Feb 2022 10:19:49 +0100 Subject: [PATCH 058/160] draft code in matrix layer to create room --- src/matrix/Session.js | 27 ++++- src/matrix/Sync.js | 2 +- src/matrix/e2ee/common.js | 12 +++ src/matrix/net/HomeServerApi.ts | 15 ++- src/matrix/room/create.ts | 165 ++++++++++++++++++++++++++++++ src/matrix/room/members/Heroes.js | 2 +- 6 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 src/matrix/room/create.ts diff --git a/src/matrix/Session.js b/src/matrix/Session.js index a2725bd4..f8d42960 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -18,6 +18,7 @@ limitations under the License. import {Room} from "./room/Room.js"; import {ArchivedRoom} from "./room/ArchivedRoom.js"; import {RoomStatus} from "./room/RoomStatus.js"; +import {RoomBeingCreated} from "./room/create"; import {Invite} from "./room/Invite.js"; import {Pusher} from "./push/Pusher"; import { ObservableMap } from "../observable/index.js"; @@ -63,6 +64,7 @@ export class Session { this._activeArchivedRooms = new Map(); this._invites = new ObservableMap(); this._inviteUpdateCallback = (invite, params) => this._invites.update(invite.id, params); + this._roomsBeingCreated = new ObservableMap(); this._user = new User(sessionInfo.userId); this._deviceMessageHandler = new DeviceMessageHandler({storage}); this._olm = olm; @@ -421,7 +423,7 @@ export class Session { // load rooms const rooms = await txn.roomSummary.getAll(); const roomLoadPromise = Promise.all(rooms.map(async summary => { - const room = this.createRoom(summary.roomId, pendingEventsByRoomId.get(summary.roomId)); + const room = this.createJoinedRoom(summary.roomId, pendingEventsByRoomId.get(summary.roomId)); await log.wrap("room", log => room.load(summary, txn, log)); this._rooms.add(room.id, room); })); @@ -530,7 +532,7 @@ export class Session { } /** @internal */ - createRoom(roomId, pendingEvents) { + createJoinedRoom(roomId, pendingEvents) { return new Room({ roomId, getSyncToken: this._getSyncToken, @@ -580,6 +582,20 @@ export class Session { }); } + get roomsBeingCreated() { + return this._roomsBeingCreated; + } + + createRoom(type, isEncrypted, explicitName, topic, invites) { + const localId = `room-being-created-${this.platform.random()}`; + const roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, explicitName, topic, invites); + this._roomsBeingCreated.set(localId, roomBeingCreated); + this._platform.logger.runDetached("create room", async log => { + roomBeingCreated.start(this._hsApi, log); + }); + return localId; + } + async obtainSyncLock(syncResponse) { const toDeviceEvents = syncResponse.to_device?.events; if (Array.isArray(toDeviceEvents) && toDeviceEvents.length) { @@ -667,6 +683,13 @@ export class Session { for (const rs of roomStates) { if (rs.shouldAdd) { this._rooms.add(rs.id, rs.room); + for (const roomBeingCreated of this._roomsBeingCreated) { + if (roomBeingCreated.roomId === rs.id) { + roomBeingCreated.notifyJoinedRoom(); + this._roomsBeingCreated.delete(roomBeingCreated.localId); + break; + } + } } else if (rs.shouldRemove) { this._rooms.remove(rs.id); } diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index de09a96d..e9faa89b 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -392,7 +392,7 @@ export class Sync { // we receive also gets written. // In any case, don't create a room for a rejected invite if (!room && (membership === "join" || (isInitialSync && membership === "leave"))) { - room = this._session.createRoom(roomId); + room = this._session.createJoinedRoom(roomId); isNewRoom = true; } if (room) { diff --git a/src/matrix/e2ee/common.js b/src/matrix/e2ee/common.js index 8b137c76..2b9d46b9 100644 --- a/src/matrix/e2ee/common.js +++ b/src/matrix/e2ee/common.js @@ -57,3 +57,15 @@ export function verifyEd25519Signature(olmUtil, userId, deviceOrKeyId, ed25519Ke return false; } } + +export function createRoomEncryptionEvent() { + return { + "type": "m.room.encryption", + "state_key": "", + "content": { + "algorithm": MEGOLM_ALGORITHM, + "rotation_period_ms": 604800000, + "rotation_period_msgs": 100 + } + } +} diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index a23321f9..bf2c2c21 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -263,20 +263,29 @@ export class HomeServerApi { return this._post(`/logout`, {}, {}, options); } - getDehydratedDevice(options: IRequestOptions): IHomeServerRequest { + getDehydratedDevice(options: IRequestOptions = {}): IHomeServerRequest { options.prefix = DEHYDRATION_PREFIX; return this._get(`/dehydrated_device`, undefined, undefined, options); } - createDehydratedDevice(payload: Record, options: IRequestOptions): IHomeServerRequest { + createDehydratedDevice(payload: Record, options: IRequestOptions = {}): IHomeServerRequest { options.prefix = DEHYDRATION_PREFIX; return this._put(`/dehydrated_device`, {}, payload, options); } - claimDehydratedDevice(deviceId: string, options: IRequestOptions): IHomeServerRequest { + claimDehydratedDevice(deviceId: string, options: IRequestOptions = {}): IHomeServerRequest { options.prefix = DEHYDRATION_PREFIX; return this._post(`/dehydrated_device/claim`, {}, {device_id: deviceId}, options); } + + profile(userId: string, options?: IRequestOptions): IHomeServerRequest { + return this._get(`/profile/${encodeURIComponent(userId)}`); + } + + createRoom(payload: Record, options?: IRequestOptions): IHomeServerRequest { + return this._post(`/createRoom`, {}, payload, options); + } + } import {Request as MockRequest} from "../../mocks/Request.js"; diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts new file mode 100644 index 00000000..934650c0 --- /dev/null +++ b/src/matrix/room/create.ts @@ -0,0 +1,165 @@ +/* +Copyright 2020 Bruno Windels + +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. +*/ + +import {calculateRoomName} from "./members/Heroes"; +import {createRoomEncryptionEvent} from "../e2ee/common"; +import {EventEmitter} from "../../utils/EventEmitter"; + +import type {StateEvent} from "../storage/types"; +import type {HomeServerApi} from "../net/HomeServerApi"; +import type {ILogItem} from "../../logging/types"; + +type CreateRoomPayload = { + is_direct?: boolean; + preset?: string; + name?: string; + topic?: string; + invite?: string[]; + initial_state?: StateEvent[] +} + +export enum RoomType { + DirectMessage, + Private, + Public +} + +function defaultE2EEStatusForType(type: RoomType): boolean { + switch (type) { + case RoomType.DirectMessage: + case RoomType.Private: + return true; + case RoomType.Public: + return false; + } +} + +function presetForType(type: RoomType): string { + switch (type) { + case RoomType.DirectMessage: + return "trusted_private_chat"; + case RoomType.Private: + return "private_chat"; + case RoomType.Public: + return "public_chat"; + } +} + +export class RoomBeingCreated extends EventEmitter<{change: never, joined: string}> { + private _roomId?: string; + private profiles: Profile[] = []; + + public readonly isEncrypted: boolean; + public readonly name: string; + + constructor( + private readonly localId: string, + private readonly type: RoomType, + isEncrypted: boolean | undefined, + private readonly explicitName: string | undefined, + private readonly topic: string | undefined, + private readonly inviteUserIds: string[] | undefined, + log: ILogItem + ) { + super(); + this.isEncrypted = isEncrypted === undefined ? defaultE2EEStatusForType(this.type) : isEncrypted; + if (explicitName) { + this.name = explicitName; + } else { + const summaryData = { + joinCount: 1, // ourselves + inviteCount: (this.inviteUserIds?.length || 0) + }; + this.name = calculateRoomName(this.profiles, summaryData, log); + } + } + + public async start(hsApi: HomeServerApi, log: ILogItem): Promise { + await Promise.all([ + this.loadProfiles(hsApi, log), + this.create(hsApi, log), + ]); + } + + private async create(hsApi: HomeServerApi, log: ILogItem): Promise { + const options: CreateRoomPayload = { + is_direct: this.type === RoomType.DirectMessage, + preset: presetForType(this.type) + }; + if (this.explicitName) { + options.name = this.explicitName; + } + if (this.topic) { + options.topic = this.topic; + } + if (this.inviteUserIds) { + options.invite = this.inviteUserIds; + } + if (this.isEncrypted) { + options.initial_state = [createRoomEncryptionEvent()]; + } + + const response = await hsApi.createRoom(options, {log}).response(); + this._roomId = response["room_id"]; + this.emit("change"); + } + + private async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise { + // only load profiles if we need it for the room name and avatar + if (!this.explicitName && this.inviteUserIds) { + this.profiles = await loadProfiles(this.inviteUserIds, hsApi, log); + this.emit("change"); + } + } + + notifyJoinedRoom() { + this.emit("joined", this._roomId); + } + + get avatarUrl(): string | undefined { + return this.profiles[0]?.avatarUrl; + } + + get roomId(): string | undefined { + return this._roomId; + } +} + +export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { + const profiles = await Promise.all(userIds.map(async userId => { + const response = await hsApi.profile(userId, {log}).response(); + return new Profile(userId, response.displayname as string, response.avatar_url as string); + })); + profiles.sort((a, b) => a.name.localeCompare(b.name)); + return profiles; +} + +interface IProfile { + get userId(): string; + get displayName(): string; + get avatarUrl(): string; + get name(): string; +} + +export class Profile implements IProfile { + constructor( + public readonly userId: string, + public readonly displayName: string, + public readonly avatarUrl: string + ) {} + + get name() { return this.displayName || this.userId; } +} diff --git a/src/matrix/room/members/Heroes.js b/src/matrix/room/members/Heroes.js index ce2fe587..1d2ab39e 100644 --- a/src/matrix/room/members/Heroes.js +++ b/src/matrix/room/members/Heroes.js @@ -16,7 +16,7 @@ limitations under the License. import {RoomMember} from "./RoomMember.js"; -function calculateRoomName(sortedMembers, summaryData, log) { +export function calculateRoomName(sortedMembers, summaryData, log) { const countWithoutMe = summaryData.joinCount + summaryData.inviteCount - 1; if (sortedMembers.length >= countWithoutMe) { if (sortedMembers.length > 1) { From 618d02d838b62abf37ac24cca6b626059be1a812 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 23 Dec 2021 15:17:09 +0530 Subject: [PATCH 059/160] fetch registration flows --- src/matrix/net/HomeServerApi.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index a23321f9..4a84adf0 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -26,6 +26,7 @@ import type {ILogItem} from "../../logging/types"; type RequestMethod = "POST" | "GET" | "PUT"; const CS_R0_PREFIX = "/_matrix/client/r0"; +const CS_V3_PREFIX = "/_matrix/client/v3"; const DEHYDRATION_PREFIX = "/_matrix/client/unstable/org.matrix.msc2697.v2"; type Options = { @@ -163,6 +164,10 @@ export class HomeServerApi { return this._unauthedRequest("GET", this._url("/login")); } + getRegistrationFlows(): IHomeServerRequest { + return this._unauthedRequest("POST", this._url("/register", CS_V3_PREFIX), undefined, { auth: {} }); + } + passwordLogin(username: string, password: string, initialDeviceDisplayName: string, options?: IRequestOptions): IHomeServerRequest { return this._unauthedRequest("POST", this._url("/login"), undefined, { "type": "m.login.password", From eb146830ba51757901469e4bb8e442deacc3ec50 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 26 Dec 2021 17:57:27 +0530 Subject: [PATCH 060/160] Implement registration endpoint --- src/matrix/net/HomeServerApi.ts | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index 4a84adf0..606514ae 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -85,7 +85,8 @@ export class HomeServerApi { body: encodedBody, timeout: options?.timeout, uploadProgress: options?.uploadProgress, - format: "json" // response format + format: "json", // response format + cache: options?.cache ?? false }); const hsRequest = new HomeServerRequest(method, url, requestResult, log); @@ -164,8 +165,25 @@ export class HomeServerApi { return this._unauthedRequest("GET", this._url("/login")); } - getRegistrationFlows(): IHomeServerRequest { - return this._unauthedRequest("POST", this._url("/register", CS_V3_PREFIX), undefined, { auth: {} }); + register(username: string, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin?: boolean , options?: IRequestOptions): IHomeServerRequest { + // todo: This is so that we disable cache-buster because it would cause the hs to respond with error + // see https://github.com/matrix-org/synapse/issues/7722 + // need to this about the implications of this later + const _options = options ?? {}; + Object.assign(_options, { cache: true }); + return this._unauthedRequest( + "POST", + this._url("/register", CS_V3_PREFIX), + undefined, + { + auth, + username, + password, + initial_device_displayname: initialDeviceDisplayName, + inhibit_login: inhibitLogin ?? false, + }, + _options + ); } passwordLogin(username: string, password: string, initialDeviceDisplayName: string, options?: IRequestOptions): IHomeServerRequest { From d28ab919bbc3cb10115cf9c1baba9600d55cc754 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 26 Dec 2021 17:59:04 +0530 Subject: [PATCH 061/160] Implement dummy registration logic --- src/matrix/registration/Registration.ts | 85 +++++++++++++++++++ .../registration/registrationStageFromType.ts | 24 ++++++ .../stages/BaseRegistrationStage.ts | 64 ++++++++++++++ src/matrix/registration/stages/DummyAuth.ts | 33 +++++++ 4 files changed, 206 insertions(+) create mode 100644 src/matrix/registration/Registration.ts create mode 100644 src/matrix/registration/registrationStageFromType.ts create mode 100644 src/matrix/registration/stages/BaseRegistrationStage.ts create mode 100644 src/matrix/registration/stages/DummyAuth.ts diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts new file mode 100644 index 00000000..9dcfdbe2 --- /dev/null +++ b/src/matrix/registration/Registration.ts @@ -0,0 +1,85 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +import type {HomeServerApi} from "../net/HomeServerApi"; +import { registrationStageFromType } from "./registrationStageFromType"; +import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; + +export type RegistrationParameters = { + username: string; + password: string; + initialDeviceDisplayName: string; + inhibitLogin: boolean; +} + +// todo: type this later +export type RegistrationResponse = { + [key: string]: any; +} + +export class Registration { + private _hsApi: HomeServerApi; + private _data: RegistrationParameters; + private _firstStage: BaseRegistrationStage; + private _session: string; + + constructor(hsApi: HomeServerApi, data: RegistrationParameters) { + this._hsApi = hsApi; + this._data = data; + } + + private async _fetchFlows(): Promise { + const response = await this._hsApi.register( + this._username, + this._password, + this._initialDeviceDisplayName, + undefined, + this._inhibitLogin).response(); + return response; + } + + async start(): Promise { + const response = await this._fetchFlows(); + this.parseStagesFromResponse(response); + await new Promise(r => setTimeout(r, 2000)); + return this._firstStage; + } + + parseStagesFromResponse(response: RegistrationResponse) { + this._session = response.session; + const flow = response.flows.pop(); + let lastStage: BaseRegistrationStage; + for (const stage of flow.stages) { + const stageClass = registrationStageFromType(stage); + if (!stageClass) { + throw new Error("Unknown stage"); + } + const registrationStage = new stageClass(this._hsApi, this._data, this._session); + if (!this._firstStage) { + this._firstStage = registrationStage; + lastStage = registrationStage; + } else { + lastStage!.setNextStage(registrationStage); + lastStage = registrationStage; + } + } + } + + private get _username() { return this._data.username; } + private get _password() { return this._data.password; } + private get _initialDeviceDisplayName() { return this._data.initialDeviceDisplayName; } + private get _inhibitLogin() { return this._data.inhibitLogin; } +} diff --git a/src/matrix/registration/registrationStageFromType.ts b/src/matrix/registration/registrationStageFromType.ts new file mode 100644 index 00000000..3a9950fd --- /dev/null +++ b/src/matrix/registration/registrationStageFromType.ts @@ -0,0 +1,24 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +import {DummyAuth} from "./stages/DummyAuth"; + +export function registrationStageFromType(type: string): typeof DummyAuth | undefined { + switch (type) { + case "m.login.dummy": + return DummyAuth; + } +} diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts new file mode 100644 index 00000000..6c5ae256 --- /dev/null +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -0,0 +1,64 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +export type Auth = { + [key: string]: any; +} + +import type {HomeServerApi} from "../../net/HomeServerApi"; +import type {RegistrationParameters, RegistrationResponse} from "../Registration"; + +export abstract class BaseRegistrationStage { + protected _hsApi: HomeServerApi; + protected _registrationData: RegistrationParameters; + protected _session: string; + protected _nextStage: BaseRegistrationStage; + + constructor(hsApi: HomeServerApi, registrationData: RegistrationParameters, session: string) { + this._hsApi = hsApi; + this._registrationData = registrationData; + this._session = session; + } + + /** + * eg: m.login.recaptcha or m.login.dummy + */ + abstract get type(): string; + + /** + * Finish a registration stage, return value is: + * - the next stage if this stage was completed successfully + * - true if registration is completed + * - an error if something went wrong + */ + abstract complete(auth?: Auth): Promise; + + setNextStage(stage: BaseRegistrationStage) { + this._nextStage = stage; + } + + parseResponse(response: RegistrationResponse) { + if (response.user_id) { + // registration completed successfully + return true; + } + else if (response.completed?.forEach(c => c === this.type)) { + return this._nextStage; + } + const error = response.error ?? "Could not parse response"; + return new Error(error); + } +} diff --git a/src/matrix/registration/stages/DummyAuth.ts b/src/matrix/registration/stages/DummyAuth.ts new file mode 100644 index 00000000..4a5d9bd8 --- /dev/null +++ b/src/matrix/registration/stages/DummyAuth.ts @@ -0,0 +1,33 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +import {BaseRegistrationStage} from "./BaseRegistrationStage"; + +export class DummyAuth extends BaseRegistrationStage { + + async complete() { + const { username, password, initialDeviceDisplayName, inhibitLogin } = this._registrationData; + const response = await this._hsApi.register(username, password, initialDeviceDisplayName, { + session: this._session, + type: this.type + }, inhibitLogin).response(); + return this.parseResponse(response); + } + + get type(): string { + return "m.login.dummy"; + } +} From d76a05952508129048cbb9e53a9939aa0be14229 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 26 Dec 2021 18:01:38 +0530 Subject: [PATCH 062/160] Temporary fix for 401 errors --- src/matrix/net/HomeServerRequest.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/matrix/net/HomeServerRequest.ts b/src/matrix/net/HomeServerRequest.ts index ea5d2e40..d9c31275 100644 --- a/src/matrix/net/HomeServerRequest.ts +++ b/src/matrix/net/HomeServerRequest.ts @@ -36,7 +36,10 @@ export class HomeServerRequest implements IHomeServerRequest { this._promise = sourceRequest.response().then(response => { log?.set("status", response.status); // ok? - if (response.status >= 200 && response.status < 300) { + // todo: register endpoint indicates our progress in using the user interactive + // authentication using 401 responses + // passing through all 401 responses as a temporary fix + if (response.status >= 200 && response.status < 300 || response.status === 401) { log?.finish(); return response.body; } else { From a59b67ec45db4b7465b358fdabb3cdb4dbd067a8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 26 Dec 2021 21:54:27 +0530 Subject: [PATCH 063/160] Fix errors --- src/matrix/registration/Registration.ts | 1 - src/matrix/registration/stages/BaseRegistrationStage.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 9dcfdbe2..1432f41a 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -54,7 +54,6 @@ export class Registration { async start(): Promise { const response = await this._fetchFlows(); this.parseStagesFromResponse(response); - await new Promise(r => setTimeout(r, 2000)); return this._firstStage; } diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 6c5ae256..51115974 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -55,7 +55,7 @@ export abstract class BaseRegistrationStage { // registration completed successfully return true; } - else if (response.completed?.forEach(c => c === this.type)) { + else if (response.completed?.find(c => c === this.type)) { return this._nextStage; } const error = response.error ?? "Could not parse response"; From 18e2fc108928f514ca8ff2c4db4abbcda16fc48d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Dec 2021 12:54:54 +0530 Subject: [PATCH 064/160] Pass in params to BaseRegistrationStage --- src/matrix/registration/Registration.ts | 5 ++--- src/matrix/registration/stages/BaseRegistrationStage.ts | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 1432f41a..312c442b 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -34,7 +34,6 @@ export class Registration { private _hsApi: HomeServerApi; private _data: RegistrationParameters; private _firstStage: BaseRegistrationStage; - private _session: string; constructor(hsApi: HomeServerApi, data: RegistrationParameters) { this._hsApi = hsApi; @@ -58,7 +57,7 @@ export class Registration { } parseStagesFromResponse(response: RegistrationResponse) { - this._session = response.session; + const { session, params } = response; const flow = response.flows.pop(); let lastStage: BaseRegistrationStage; for (const stage of flow.stages) { @@ -66,7 +65,7 @@ export class Registration { if (!stageClass) { throw new Error("Unknown stage"); } - const registrationStage = new stageClass(this._hsApi, this._data, this._session); + const registrationStage = new stageClass(this._hsApi, this._data, session, params?.[stage]); if (!this._firstStage) { this._firstStage = registrationStage; lastStage = registrationStage; diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 51115974..1458c554 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -26,11 +26,13 @@ export abstract class BaseRegistrationStage { protected _registrationData: RegistrationParameters; protected _session: string; protected _nextStage: BaseRegistrationStage; + protected _params?: Record - constructor(hsApi: HomeServerApi, registrationData: RegistrationParameters, session: string) { + constructor(hsApi: HomeServerApi, registrationData: RegistrationParameters, session: string, params?: Record) { this._hsApi = hsApi; this._registrationData = registrationData; this._session = session; + this._params = params; } /** From 8ab8726b8f0d57757b17c92998ced34a99c2c6c5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 27 Dec 2021 13:14:07 +0530 Subject: [PATCH 065/160] Implement m.login.terms stage --- .../registration/registrationStageFromType.ts | 10 ++++- src/matrix/registration/stages/TermsAuth.ts | 39 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/matrix/registration/stages/TermsAuth.ts diff --git a/src/matrix/registration/registrationStageFromType.ts b/src/matrix/registration/registrationStageFromType.ts index 3a9950fd..44913f94 100644 --- a/src/matrix/registration/registrationStageFromType.ts +++ b/src/matrix/registration/registrationStageFromType.ts @@ -14,11 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ +import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; +import type {HomeServerApi} from "../net/HomeServerApi"; +import type {RegistrationParameters} from "./Registration"; import {DummyAuth} from "./stages/DummyAuth"; +import {TermsAuth} from "./stages/TermsAuth"; -export function registrationStageFromType(type: string): typeof DummyAuth | undefined { +type DerivedFromBaseRegistration = { new(hsApi: HomeServerApi, registrationData: RegistrationParameters, session: string, params?: Record): BaseRegistrationStage } & typeof BaseRegistrationStage | undefined; + +export function registrationStageFromType(type: string): DerivedFromBaseRegistration { switch (type) { case "m.login.dummy": return DummyAuth; + case "m.login.terms": + return TermsAuth; } } diff --git a/src/matrix/registration/stages/TermsAuth.ts b/src/matrix/registration/stages/TermsAuth.ts new file mode 100644 index 00000000..0114d27d --- /dev/null +++ b/src/matrix/registration/stages/TermsAuth.ts @@ -0,0 +1,39 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +import {BaseRegistrationStage} from "./BaseRegistrationStage"; + +export class TermsAuth extends BaseRegistrationStage { + + async complete() { + const { username, password, initialDeviceDisplayName, inhibitLogin } = this._registrationData; + const response = await this._hsApi.register(username, password, initialDeviceDisplayName, { + session: this._session, + type: this.type + }, inhibitLogin).response(); + return this.parseResponse(response); + } + + get type(): string { + return "m.login.terms"; + } + + // todo: add better parsing logic here, also remember that server admins can + // require any sort documents here, even those that are not toc/privacy policies + get privacyPolicy() { + return this._params?.policies["privacy_policy"]; + } +} From 170d7a5e555358eb93d307d4579fe0806618f705 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 5 Jan 2022 12:45:41 +0530 Subject: [PATCH 066/160] Add startRegistration method --- src/matrix/Client.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/matrix/Client.js b/src/matrix/Client.js index f070d24c..bc35703a 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -30,6 +30,7 @@ import {PasswordLoginMethod} from "./login/PasswordLoginMethod"; import {TokenLoginMethod} from "./login/TokenLoginMethod"; import {SSOLoginHelper} from "./login/SSOLoginHelper"; import {getDehydratedDevice} from "./e2ee/Dehydration.js"; +import {Registration} from "./registration/Registration"; export const LoadStatus = createEnum( "NotLoading", @@ -131,6 +132,19 @@ export class Client { }); } + async startRegistration(homeserver, username, password, initialDeviceDisplayName) { + const request = this._platform.request; + const hsApi = new HomeServerApi({homeserver, request}); + const registration = new Registration(hsApi, { + username, + password, + initialDeviceDisplayName, + inhibitLogin: true + }); + let stage = await registration.start(); + return stage; + } + async startWithLogin(loginMethod, {inspectAccountSetup} = {}) { const currentStatus = this._status.get(); if (currentStatus !== LoadStatus.LoginFailed && From fa2e2bc8f33b0af8b0a649f8a9cc2e25c182a5b8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 20 Jan 2022 12:34:36 +0530 Subject: [PATCH 067/160] Allow register without providing username --- src/matrix/net/HomeServerApi.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index 606514ae..62008087 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -165,19 +165,20 @@ export class HomeServerApi { return this._unauthedRequest("GET", this._url("/login")); } - register(username: string, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin?: boolean , options?: IRequestOptions): IHomeServerRequest { + register(username: string | null, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin?: boolean , options?: IRequestOptions): IHomeServerRequest { // todo: This is so that we disable cache-buster because it would cause the hs to respond with error // see https://github.com/matrix-org/synapse/issues/7722 // need to this about the implications of this later const _options = options ?? {}; Object.assign(_options, { cache: true }); + const _username = username ?? undefined; return this._unauthedRequest( "POST", this._url("/register", CS_V3_PREFIX), undefined, { auth, - username, + _username, password, initial_device_displayname: initialDeviceDisplayName, inhibit_login: inhibitLogin ?? false, From 792d5c62c52775d1b34fd73b4ed5ca7910ad059d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 20 Jan 2022 12:35:11 +0530 Subject: [PATCH 068/160] Return username when registration is completed --- src/matrix/registration/stages/BaseRegistrationStage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 1458c554..4c15bb5e 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -43,10 +43,10 @@ export abstract class BaseRegistrationStage { /** * Finish a registration stage, return value is: * - the next stage if this stage was completed successfully - * - true if registration is completed + * - user-id (string) if registration is completed * - an error if something went wrong */ - abstract complete(auth?: Auth): Promise; + abstract complete(auth?: Auth): Promise; setNextStage(stage: BaseRegistrationStage) { this._nextStage = stage; @@ -55,7 +55,7 @@ export abstract class BaseRegistrationStage { parseResponse(response: RegistrationResponse) { if (response.user_id) { // registration completed successfully - return true; + return response.user_id; } else if (response.completed?.find(c => c === this.type)) { return this._nextStage; From 420c12f2023dc16003dc32c48ec302d3a2994e31 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 12:52:52 +0530 Subject: [PATCH 069/160] Copy over username only if it exists --- src/matrix/net/HomeServerApi.ts | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index 62008087..6a0e0bca 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -168,23 +168,19 @@ export class HomeServerApi { register(username: string | null, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin?: boolean , options?: IRequestOptions): IHomeServerRequest { // todo: This is so that we disable cache-buster because it would cause the hs to respond with error // see https://github.com/matrix-org/synapse/issues/7722 - // need to this about the implications of this later const _options = options ?? {}; Object.assign(_options, { cache: true }); - const _username = username ?? undefined; - return this._unauthedRequest( - "POST", - this._url("/register", CS_V3_PREFIX), - undefined, - { - auth, - _username, - password, - initial_device_displayname: initialDeviceDisplayName, - inhibit_login: inhibitLogin ?? false, - }, - _options - ); + const body = { + auth, + password, + initial_device_displayname: initialDeviceDisplayName, + inhibit_login: inhibitLogin ?? false, + }; + if (username) { + // username is optional for registration + Object.assign(body, { username }); + } + return this._unauthedRequest( "POST", this._url("/register", CS_V3_PREFIX), undefined, body, _options); } passwordLogin(username: string, password: string, initialDeviceDisplayName: string, options?: IRequestOptions): IHomeServerRequest { From bb6a885116bd72d64c52679589c8cde682b1926c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 15:38:55 +0530 Subject: [PATCH 070/160] Specify what errors are ignored in options --- src/matrix/net/HomeServerApi.ts | 13 ++----------- src/matrix/net/HomeServerRequest.ts | 13 ++++++++----- src/platform/types/types.ts | 1 + 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index 6a0e0bca..a722da30 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -58,15 +58,6 @@ export class HomeServerApi { private _baseRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: IRequestOptions, accessToken?: string): IHomeServerRequest { const queryString = encodeQueryParams(queryParams); url = `${url}?${queryString}`; - let log: ILogItem | undefined; - if (options?.log) { - const parent = options?.log; - log = parent.child({ - t: "network", - url, - method, - }, parent.level.Info); - } let encodedBody: EncodedBody["body"]; const headers: Map = new Map(); if (accessToken) { @@ -89,7 +80,7 @@ export class HomeServerApi { cache: options?.cache ?? false }); - const hsRequest = new HomeServerRequest(method, url, requestResult, log); + const hsRequest = new HomeServerRequest(method, url, requestResult, options); if (this._reconnector) { hsRequest.response().catch(err => { @@ -169,7 +160,7 @@ export class HomeServerApi { // todo: This is so that we disable cache-buster because it would cause the hs to respond with error // see https://github.com/matrix-org/synapse/issues/7722 const _options = options ?? {}; - Object.assign(_options, { cache: true }); + Object.assign(_options, { cache: true, allowedErrors: [401] }); const body = { auth, password, diff --git a/src/matrix/net/HomeServerRequest.ts b/src/matrix/net/HomeServerRequest.ts index d9c31275..cbbb4e90 100644 --- a/src/matrix/net/HomeServerRequest.ts +++ b/src/matrix/net/HomeServerRequest.ts @@ -18,6 +18,7 @@ limitations under the License. import {HomeServerError, ConnectionError} from "../error.js"; import type {RequestResult} from "../../platform/web/dom/request/fetch.js"; import type {ILogItem} from "../../logging/types"; +import type {IRequestOptions} from "../../platform/types/types.js"; export interface IHomeServerRequest { abort(): void; @@ -30,16 +31,18 @@ export class HomeServerRequest implements IHomeServerRequest { // as we add types for expected responses from hs, this could be a generic class instead private readonly _promise: Promise; - constructor(method: string, url: string, sourceRequest: RequestResult, log?: ILogItem) { + constructor(method: string, url: string, sourceRequest: RequestResult, options?: IRequestOptions) { + let log: ILogItem | undefined; + if (options?.log) { + const parent = options?.log; + log = parent.child({ t: "network", url, method, }, parent.level.Info); + } this._log = log; this._sourceRequest = sourceRequest; this._promise = sourceRequest.response().then(response => { log?.set("status", response.status); // ok? - // todo: register endpoint indicates our progress in using the user interactive - // authentication using 401 responses - // passing through all 401 responses as a temporary fix - if (response.status >= 200 && response.status < 300 || response.status === 401) { + if (response.status >= 200 && response.status < 300 || options?.allowedErrors?.find(e => e === response.status)) { log?.finish(); return response.body; } else { diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts index 58d2216f..0ad569b4 100644 --- a/src/platform/types/types.ts +++ b/src/platform/types/types.ts @@ -28,6 +28,7 @@ export interface IRequestOptions { prefix?: string; method?: string; format?: string; + allowedErrors?: number[]; } export type RequestFunction = (url: string, options: IRequestOptions) => RequestResult; From 5e93e048ab83532f772c2ef0bfd2d141986341af Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 17:07:32 +0530 Subject: [PATCH 071/160] Don't cache GET requests --- src/matrix/net/HomeServerApi.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index a722da30..45e8a56c 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -77,7 +77,7 @@ export class HomeServerApi { timeout: options?.timeout, uploadProgress: options?.uploadProgress, format: "json", // response format - cache: options?.cache ?? false + cache: method !== "GET", }); const hsRequest = new HomeServerRequest(method, url, requestResult, options); @@ -160,7 +160,7 @@ export class HomeServerApi { // todo: This is so that we disable cache-buster because it would cause the hs to respond with error // see https://github.com/matrix-org/synapse/issues/7722 const _options = options ?? {}; - Object.assign(_options, { cache: true, allowedErrors: [401] }); + Object.assign(_options, { allowedErrors: [401] }); const body = { auth, password, From 755f934eb28b2104611385c2098bcd5c482dc2bf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 17:40:09 +0530 Subject: [PATCH 072/160] No need to explicitly pass in inhibitLogin --- src/matrix/Client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/matrix/Client.js b/src/matrix/Client.js index bc35703a..22e686d0 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -139,7 +139,6 @@ export class Client { username, password, initialDeviceDisplayName, - inhibitLogin: true }); let stage = await registration.start(); return stage; From e8dbbd876cf2ccb4b40c75b7a441dc972de5d1fb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 17:45:14 +0530 Subject: [PATCH 073/160] Give default values to parameters --- src/matrix/net/HomeServerApi.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index 45e8a56c..e5071b87 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -156,22 +156,19 @@ export class HomeServerApi { return this._unauthedRequest("GET", this._url("/login")); } - register(username: string | null, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin?: boolean , options?: IRequestOptions): IHomeServerRequest { - // todo: This is so that we disable cache-buster because it would cause the hs to respond with error - // see https://github.com/matrix-org/synapse/issues/7722 - const _options = options ?? {}; - Object.assign(_options, { allowedErrors: [401] }); + register(username: string | null, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin: boolean = true , options: IRequestOptions = {}): IHomeServerRequest { + Object.assign(options, { allowedErrors: [401] }); const body = { auth, password, initial_device_displayname: initialDeviceDisplayName, - inhibit_login: inhibitLogin ?? false, + inhibit_login: inhibitLogin, }; if (username) { // username is optional for registration Object.assign(body, { username }); } - return this._unauthedRequest( "POST", this._url("/register", CS_V3_PREFIX), undefined, body, _options); + return this._unauthedRequest( "POST", this._url("/register", CS_V3_PREFIX), undefined, body, options); } passwordLogin(username: string, password: string, initialDeviceDisplayName: string, options?: IRequestOptions): IHomeServerRequest { From 5f11790f6bd9b624f41be501e56c2afaca2aca0a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 17:51:37 +0530 Subject: [PATCH 074/160] Object.assign is overkill here --- src/matrix/net/HomeServerApi.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index e5071b87..7f7dd16c 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -158,7 +158,7 @@ export class HomeServerApi { register(username: string | null, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin: boolean = true , options: IRequestOptions = {}): IHomeServerRequest { Object.assign(options, { allowedErrors: [401] }); - const body = { + const body: any = { auth, password, initial_device_displayname: initialDeviceDisplayName, @@ -166,7 +166,7 @@ export class HomeServerApi { }; if (username) { // username is optional for registration - Object.assign(body, { username }); + body.username = username; } return this._unauthedRequest( "POST", this._url("/register", CS_V3_PREFIX), undefined, body, options); } From 550a560f40330468a7ce05a59e27c295702f842b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 17:54:08 +0530 Subject: [PATCH 075/160] Remove space --- src/matrix/registration/Registration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 312c442b..72f0c401 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -15,7 +15,7 @@ limitations under the License. */ import type {HomeServerApi} from "../net/HomeServerApi"; -import { registrationStageFromType } from "./registrationStageFromType"; +import {registrationStageFromType} from "./registrationStageFromType"; import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; export type RegistrationParameters = { From a91ba4370df3e218e5cc181098d5cbd20365b38e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 17:56:43 +0530 Subject: [PATCH 076/160] Change type to show that username is optional --- src/matrix/registration/Registration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 72f0c401..d0f11a1c 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -19,7 +19,7 @@ import {registrationStageFromType} from "./registrationStageFromType"; import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; export type RegistrationParameters = { - username: string; + username: string | null; password: string; initialDeviceDisplayName: string; inhibitLogin: boolean; From 2d4c1065427e6c1b6033face676d3d359694e1a8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 18:01:33 +0530 Subject: [PATCH 077/160] REFACTOR: Inline method --- src/matrix/registration/Registration.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index d0f11a1c..e9c0b512 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -40,18 +40,13 @@ export class Registration { this._data = data; } - private async _fetchFlows(): Promise { + async start(): Promise { const response = await this._hsApi.register( this._username, this._password, this._initialDeviceDisplayName, undefined, this._inhibitLogin).response(); - return response; - } - - async start(): Promise { - const response = await this._fetchFlows(); this.parseStagesFromResponse(response); return this._firstStage; } From 2f3865d8cc8407c4a1adc27e7155ac6279338e5a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 18:23:41 +0530 Subject: [PATCH 078/160] firstStage should be a local variable --- src/matrix/registration/Registration.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index e9c0b512..c6e26604 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -33,7 +33,6 @@ export type RegistrationResponse = { export class Registration { private _hsApi: HomeServerApi; private _data: RegistrationParameters; - private _firstStage: BaseRegistrationStage; constructor(hsApi: HomeServerApi, data: RegistrationParameters) { this._hsApi = hsApi; @@ -47,13 +46,13 @@ export class Registration { this._initialDeviceDisplayName, undefined, this._inhibitLogin).response(); - this.parseStagesFromResponse(response); - return this._firstStage; + return this.parseStagesFromResponse(response); } - parseStagesFromResponse(response: RegistrationResponse) { + parseStagesFromResponse(response: RegistrationResponse): BaseRegistrationStage { const { session, params } = response; const flow = response.flows.pop(); + let firstStage: BaseRegistrationStage | undefined; let lastStage: BaseRegistrationStage; for (const stage of flow.stages) { const stageClass = registrationStageFromType(stage); @@ -61,14 +60,15 @@ export class Registration { throw new Error("Unknown stage"); } const registrationStage = new stageClass(this._hsApi, this._data, session, params?.[stage]); - if (!this._firstStage) { - this._firstStage = registrationStage; + if (!firstStage) { + firstStage = registrationStage; lastStage = registrationStage; } else { lastStage!.setNextStage(registrationStage); lastStage = registrationStage; } } + return firstStage!; } private get _username() { return this._data.username; } From 5de1fc145374eeadcad05a660d17010bccceb67b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 18:26:20 +0530 Subject: [PATCH 079/160] Remove unnecessary getters --- src/matrix/registration/Registration.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index c6e26604..59a6bd95 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -41,11 +41,11 @@ export class Registration { async start(): Promise { const response = await this._hsApi.register( - this._username, - this._password, - this._initialDeviceDisplayName, + this._data.username, + this._data.password, + this._data.initialDeviceDisplayName, undefined, - this._inhibitLogin).response(); + this._data.inhibitLogin).response(); return this.parseStagesFromResponse(response); } @@ -70,9 +70,4 @@ export class Registration { } return firstStage!; } - - private get _username() { return this._data.username; } - private get _password() { return this._data.password; } - private get _initialDeviceDisplayName() { return this._data.initialDeviceDisplayName; } - private get _inhibitLogin() { return this._data.inhibitLogin; } } From 6eba60bd75fb22dd712d4d937b5f42bbb1323e04 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 19:26:51 +0530 Subject: [PATCH 080/160] Use typescript style that was agreed on earlier --- src/matrix/registration/stages/BaseRegistrationStage.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 4c15bb5e..bea95598 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -18,6 +18,10 @@ export type Auth = { [key: string]: any; } +type Params = { + [key: string]: any; +} + import type {HomeServerApi} from "../../net/HomeServerApi"; import type {RegistrationParameters, RegistrationResponse} from "../Registration"; @@ -28,7 +32,7 @@ export abstract class BaseRegistrationStage { protected _nextStage: BaseRegistrationStage; protected _params?: Record - constructor(hsApi: HomeServerApi, registrationData: RegistrationParameters, session: string, params?: Record) { + constructor(hsApi: HomeServerApi, registrationData: RegistrationParameters, session: string, params?: Params) { this._hsApi = hsApi; this._registrationData = registrationData; this._session = session; From 7bb7189c6ae65003490601eae55c4eea6685f802 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 1 Feb 2022 19:29:11 +0530 Subject: [PATCH 081/160] No need for this export --- src/matrix/registration/stages/BaseRegistrationStage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index bea95598..6ffd9187 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -export type Auth = { +type Auth = { [key: string]: any; } From ac7108b882d8a3ac19093f83a6c498e742d5bda2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 2 Feb 2022 12:25:12 +0530 Subject: [PATCH 082/160] Throw error instead of returning it --- src/matrix/registration/stages/BaseRegistrationStage.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 6ffd9187..df23c429 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -48,9 +48,8 @@ export abstract class BaseRegistrationStage { * Finish a registration stage, return value is: * - the next stage if this stage was completed successfully * - user-id (string) if registration is completed - * - an error if something went wrong */ - abstract complete(auth?: Auth): Promise; + abstract complete(auth?: Auth): Promise; setNextStage(stage: BaseRegistrationStage) { this._nextStage = stage; @@ -65,6 +64,6 @@ export abstract class BaseRegistrationStage { return this._nextStage; } const error = response.error ?? "Could not parse response"; - return new Error(error); - } + throw new Error(error); + } } From b482d478b46fab8ec1c8ef91f0e70b141d9eadcd Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 2 Feb 2022 12:25:34 +0530 Subject: [PATCH 083/160] Add a tos getter --- src/matrix/registration/stages/TermsAuth.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/matrix/registration/stages/TermsAuth.ts b/src/matrix/registration/stages/TermsAuth.ts index 0114d27d..7d1647b9 100644 --- a/src/matrix/registration/stages/TermsAuth.ts +++ b/src/matrix/registration/stages/TermsAuth.ts @@ -31,9 +31,11 @@ export class TermsAuth extends BaseRegistrationStage { return "m.login.terms"; } - // todo: add better parsing logic here, also remember that server admins can - // require any sort documents here, even those that are not toc/privacy policies get privacyPolicy() { return this._params?.policies["privacy_policy"]; } + + get termsOfService() { + return this._params?.policies["terms_of_service"]; + } } From 49ade61ef6e2b8fa686dbc4510098975f1df984a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 2 Feb 2022 12:45:49 +0530 Subject: [PATCH 084/160] Fill in ts types + change names --- src/matrix/registration/Registration.ts | 6 +++--- .../registration/registrationStageFromType.ts | 4 ++-- .../registration/stages/BaseRegistrationStage.ts | 15 +++++++++------ 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 59a6bd95..dd19fae4 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -18,7 +18,7 @@ import type {HomeServerApi} from "../net/HomeServerApi"; import {registrationStageFromType} from "./registrationStageFromType"; import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; -export type RegistrationParameters = { +export type RegistrationDetails = { username: string | null; password: string; initialDeviceDisplayName: string; @@ -32,9 +32,9 @@ export type RegistrationResponse = { export class Registration { private _hsApi: HomeServerApi; - private _data: RegistrationParameters; + private _data: RegistrationDetails; - constructor(hsApi: HomeServerApi, data: RegistrationParameters) { + constructor(hsApi: HomeServerApi, data: RegistrationDetails) { this._hsApi = hsApi; this._data = data; } diff --git a/src/matrix/registration/registrationStageFromType.ts b/src/matrix/registration/registrationStageFromType.ts index 44913f94..31330bc9 100644 --- a/src/matrix/registration/registrationStageFromType.ts +++ b/src/matrix/registration/registrationStageFromType.ts @@ -16,11 +16,11 @@ limitations under the License. import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; import type {HomeServerApi} from "../net/HomeServerApi"; -import type {RegistrationParameters} from "./Registration"; +import type {RegistrationDetails} from "./Registration"; import {DummyAuth} from "./stages/DummyAuth"; import {TermsAuth} from "./stages/TermsAuth"; -type DerivedFromBaseRegistration = { new(hsApi: HomeServerApi, registrationData: RegistrationParameters, session: string, params?: Record): BaseRegistrationStage } & typeof BaseRegistrationStage | undefined; +type DerivedFromBaseRegistration = { new(hsApi: HomeServerApi, registrationData: RegistrationDetails, session: string, params?: Record): BaseRegistrationStage } & typeof BaseRegistrationStage | undefined; export function registrationStageFromType(type: string): DerivedFromBaseRegistration { switch (type) { diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index df23c429..57c0fa0e 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -14,25 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. */ -type Auth = { +type AuthenticationData = { + type: string; + session: string; [key: string]: any; } -type Params = { +// contains data needed to complete this stage, eg: link to privacy policy +type RegistrationParams = { [key: string]: any; } import type {HomeServerApi} from "../../net/HomeServerApi"; -import type {RegistrationParameters, RegistrationResponse} from "../Registration"; +import type {RegistrationDetails, RegistrationResponse} from "../Registration"; export abstract class BaseRegistrationStage { protected _hsApi: HomeServerApi; - protected _registrationData: RegistrationParameters; + protected _registrationData: RegistrationDetails; protected _session: string; protected _nextStage: BaseRegistrationStage; protected _params?: Record - constructor(hsApi: HomeServerApi, registrationData: RegistrationParameters, session: string, params?: Params) { + constructor(hsApi: HomeServerApi, registrationData: RegistrationDetails, session: string, params?: RegistrationParams) { this._hsApi = hsApi; this._registrationData = registrationData; this._session = session; @@ -49,7 +52,7 @@ export abstract class BaseRegistrationStage { * - the next stage if this stage was completed successfully * - user-id (string) if registration is completed */ - abstract complete(auth?: Auth): Promise; + abstract complete(auth?: AuthenticationData): Promise; setNextStage(stage: BaseRegistrationStage) { this._nextStage = stage; From 1d4b079d0c815c712f4fe30e790bab16d6dadd7c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 2 Feb 2022 13:58:03 +0530 Subject: [PATCH 085/160] Type RegistrationResponse --- src/matrix/registration/Registration.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index dd19fae4..cd44dc38 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -23,11 +23,23 @@ export type RegistrationDetails = { password: string; initialDeviceDisplayName: string; inhibitLogin: boolean; -} +} -// todo: type this later export type RegistrationResponse = { - [key: string]: any; + completed: string[]; + flows: Record[]; + params: Record; + session: string; +} & error & success; + +type error = { + errcode: string; + error: string; +} +type success = { + user_id: string; + device_id: string; + access_token?: string; } export class Registration { @@ -52,6 +64,9 @@ export class Registration { parseStagesFromResponse(response: RegistrationResponse): BaseRegistrationStage { const { session, params } = response; const flow = response.flows.pop(); + if (!flow) { + throw new Error("No registration flows available!"); + } let firstStage: BaseRegistrationStage | undefined; let lastStage: BaseRegistrationStage; for (const stage of flow.stages) { From 3a67da88305afe1636a5c48cd882bf78bb89b2c3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 2 Feb 2022 14:54:10 +0530 Subject: [PATCH 086/160] Refactor type - Change name - Move union type down --- src/matrix/registration/registrationStageFromType.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/registration/registrationStageFromType.ts b/src/matrix/registration/registrationStageFromType.ts index 31330bc9..45375516 100644 --- a/src/matrix/registration/registrationStageFromType.ts +++ b/src/matrix/registration/registrationStageFromType.ts @@ -20,9 +20,9 @@ import type {RegistrationDetails} from "./Registration"; import {DummyAuth} from "./stages/DummyAuth"; import {TermsAuth} from "./stages/TermsAuth"; -type DerivedFromBaseRegistration = { new(hsApi: HomeServerApi, registrationData: RegistrationDetails, session: string, params?: Record): BaseRegistrationStage } & typeof BaseRegistrationStage | undefined; +type ClassDerivedFromBaseRegistration = { new(hsApi: HomeServerApi, registrationData: RegistrationDetails, session: string, params?: Record): BaseRegistrationStage } & typeof BaseRegistrationStage; -export function registrationStageFromType(type: string): DerivedFromBaseRegistration { +export function registrationStageFromType(type: string): ClassDerivedFromBaseRegistration | undefined{ switch (type) { case "m.login.dummy": return DummyAuth; From 6798a5e429d97e2dcdf7132c2bb748d0852eb69c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 2 Feb 2022 15:05:03 +0530 Subject: [PATCH 087/160] Move types to types.ts --- src/matrix/registration/Registration.ts | 25 +-------- .../registration/registrationStageFromType.ts | 2 +- .../stages/BaseRegistrationStage.ts | 13 +---- src/matrix/registration/types/type.ts | 55 +++++++++++++++++++ 4 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 src/matrix/registration/types/type.ts diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index cd44dc38..7fb263f3 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -17,30 +17,7 @@ limitations under the License. import type {HomeServerApi} from "../net/HomeServerApi"; import {registrationStageFromType} from "./registrationStageFromType"; import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; - -export type RegistrationDetails = { - username: string | null; - password: string; - initialDeviceDisplayName: string; - inhibitLogin: boolean; -} - -export type RegistrationResponse = { - completed: string[]; - flows: Record[]; - params: Record; - session: string; -} & error & success; - -type error = { - errcode: string; - error: string; -} -type success = { - user_id: string; - device_id: string; - access_token?: string; -} +import type {RegistrationDetails, RegistrationResponse} from "./types/type"; export class Registration { private _hsApi: HomeServerApi; diff --git a/src/matrix/registration/registrationStageFromType.ts b/src/matrix/registration/registrationStageFromType.ts index 45375516..28d5c2b0 100644 --- a/src/matrix/registration/registrationStageFromType.ts +++ b/src/matrix/registration/registrationStageFromType.ts @@ -16,7 +16,7 @@ limitations under the License. import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; import type {HomeServerApi} from "../net/HomeServerApi"; -import type {RegistrationDetails} from "./Registration"; +import type {RegistrationDetails} from "./types/type"; import {DummyAuth} from "./stages/DummyAuth"; import {TermsAuth} from "./stages/TermsAuth"; diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 57c0fa0e..2dfc0628 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -14,19 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -type AuthenticationData = { - type: string; - session: string; - [key: string]: any; -} - -// contains data needed to complete this stage, eg: link to privacy policy -type RegistrationParams = { - [key: string]: any; -} - import type {HomeServerApi} from "../../net/HomeServerApi"; -import type {RegistrationDetails, RegistrationResponse} from "../Registration"; +import type {RegistrationDetails, RegistrationResponse, AuthenticationData, RegistrationParams} from "../types/type"; export abstract class BaseRegistrationStage { protected _hsApi: HomeServerApi; diff --git a/src/matrix/registration/types/type.ts b/src/matrix/registration/types/type.ts new file mode 100644 index 00000000..2863ec28 --- /dev/null +++ b/src/matrix/registration/types/type.ts @@ -0,0 +1,55 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +export type RegistrationDetails = { + username: string | null; + password: string; + initialDeviceDisplayName: string; + inhibitLogin: boolean; +} + +export type RegistrationResponse = RegistrationResponse401 & RegistrationResponseError & RegistrationResponseSuccess; + +type RegistrationResponse401 = { + completed: string[]; + flows: Record[]; + params: Record; + session: string; +} + +type RegistrationResponseError = { + errcode: string; + error: string; +} + +type RegistrationResponseSuccess = { + user_id: string; + device_id: string; + access_token?: string; +} + +/* Types for Registration Stage */ + +export type AuthenticationData = { + type: string; + session: string; + [key: string]: any; +} + +// contains additional data needed to complete a stage, eg: link to privacy policy +export type RegistrationParams = { + [key: string]: any; +} From a249a1b2b517b3959f948b200ef4e84b8844464d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 2 Feb 2022 15:51:02 +0530 Subject: [PATCH 088/160] Implement flow seclector --- src/matrix/registration/Registration.ts | 14 +++++++++----- .../registration/registrationStageFromType.ts | 2 +- .../registration/stages/BaseRegistrationStage.ts | 2 +- .../registration/types/{type.ts => types.ts} | 6 +++++- 4 files changed, 16 insertions(+), 8 deletions(-) rename src/matrix/registration/types/{type.ts => types.ts} (94%) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 7fb263f3..15006ce2 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -17,15 +17,19 @@ limitations under the License. import type {HomeServerApi} from "../net/HomeServerApi"; import {registrationStageFromType} from "./registrationStageFromType"; import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; -import type {RegistrationDetails, RegistrationResponse} from "./types/type"; +import type {RegistrationDetails, RegistrationResponse, RegistrationFlow} from "./types/types"; + +type FlowSelector = (flows: RegistrationFlow[]) => RegistrationFlow | void; export class Registration { private _hsApi: HomeServerApi; private _data: RegistrationDetails; + private _flowSelector: FlowSelector; - constructor(hsApi: HomeServerApi, data: RegistrationDetails) { + constructor(hsApi: HomeServerApi, data: RegistrationDetails, flowSelector?: FlowSelector) { this._hsApi = hsApi; this._data = data; + this._flowSelector = flowSelector ?? (flows => flows.pop()); } async start(): Promise { @@ -40,16 +44,16 @@ export class Registration { parseStagesFromResponse(response: RegistrationResponse): BaseRegistrationStage { const { session, params } = response; - const flow = response.flows.pop(); + const flow = this._flowSelector(response.flows); if (!flow) { - throw new Error("No registration flows available!"); + throw new Error("flowSelector did not return any flow!"); } let firstStage: BaseRegistrationStage | undefined; let lastStage: BaseRegistrationStage; for (const stage of flow.stages) { const stageClass = registrationStageFromType(stage); if (!stageClass) { - throw new Error("Unknown stage"); + throw new Error(`Unknown stage: ${stage}`); } const registrationStage = new stageClass(this._hsApi, this._data, session, params?.[stage]); if (!firstStage) { diff --git a/src/matrix/registration/registrationStageFromType.ts b/src/matrix/registration/registrationStageFromType.ts index 28d5c2b0..9552d049 100644 --- a/src/matrix/registration/registrationStageFromType.ts +++ b/src/matrix/registration/registrationStageFromType.ts @@ -16,7 +16,7 @@ limitations under the License. import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; import type {HomeServerApi} from "../net/HomeServerApi"; -import type {RegistrationDetails} from "./types/type"; +import type {RegistrationDetails} from "./types/types"; import {DummyAuth} from "./stages/DummyAuth"; import {TermsAuth} from "./stages/TermsAuth"; diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 2dfc0628..62325b4c 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -15,7 +15,7 @@ limitations under the License. */ import type {HomeServerApi} from "../../net/HomeServerApi"; -import type {RegistrationDetails, RegistrationResponse, AuthenticationData, RegistrationParams} from "../types/type"; +import type {RegistrationDetails, RegistrationResponse, AuthenticationData, RegistrationParams} from "../types/types"; export abstract class BaseRegistrationStage { protected _hsApi: HomeServerApi; diff --git a/src/matrix/registration/types/type.ts b/src/matrix/registration/types/types.ts similarity index 94% rename from src/matrix/registration/types/type.ts rename to src/matrix/registration/types/types.ts index 2863ec28..97dca1be 100644 --- a/src/matrix/registration/types/type.ts +++ b/src/matrix/registration/types/types.ts @@ -25,7 +25,7 @@ export type RegistrationResponse = RegistrationResponse401 & RegistrationRespons type RegistrationResponse401 = { completed: string[]; - flows: Record[]; + flows: RegistrationFlow[]; params: Record; session: string; } @@ -41,6 +41,10 @@ type RegistrationResponseSuccess = { access_token?: string; } +export type RegistrationFlow = { + stages: string[]; +} + /* Types for Registration Stage */ export type AuthenticationData = { From fe0add01ee7c24c0b91781db062109424a5366c3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 2 Feb 2022 16:09:01 +0530 Subject: [PATCH 089/160] Use union of types for RegistrationResponse --- src/matrix/registration/Registration.ts | 4 ++-- src/matrix/registration/stages/BaseRegistrationStage.ts | 6 +++--- src/matrix/registration/types/types.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 15006ce2..8b4d54f9 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -17,7 +17,7 @@ limitations under the License. import type {HomeServerApi} from "../net/HomeServerApi"; import {registrationStageFromType} from "./registrationStageFromType"; import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; -import type {RegistrationDetails, RegistrationResponse, RegistrationFlow} from "./types/types"; +import type {RegistrationDetails, RegistrationFlow, RegistrationResponse401} from "./types/types"; type FlowSelector = (flows: RegistrationFlow[]) => RegistrationFlow | void; @@ -42,7 +42,7 @@ export class Registration { return this.parseStagesFromResponse(response); } - parseStagesFromResponse(response: RegistrationResponse): BaseRegistrationStage { + parseStagesFromResponse(response: RegistrationResponse401): BaseRegistrationStage { const { session, params } = response; const flow = this._flowSelector(response.flows); if (!flow) { diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 62325b4c..7b41379c 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -48,14 +48,14 @@ export abstract class BaseRegistrationStage { } parseResponse(response: RegistrationResponse) { - if (response.user_id) { + if ("user_id" in response) { // registration completed successfully return response.user_id; } - else if (response.completed?.find(c => c === this.type)) { + else if ("completed" in response && response.completed?.find(c => c === this.type)) { return this._nextStage; } - const error = response.error ?? "Could not parse response"; + const error = "error" in response? response.error: "Could not parse response"; throw new Error(error); } } diff --git a/src/matrix/registration/types/types.ts b/src/matrix/registration/types/types.ts index 97dca1be..5661e268 100644 --- a/src/matrix/registration/types/types.ts +++ b/src/matrix/registration/types/types.ts @@ -21,9 +21,9 @@ export type RegistrationDetails = { inhibitLogin: boolean; } -export type RegistrationResponse = RegistrationResponse401 & RegistrationResponseError & RegistrationResponseSuccess; +export type RegistrationResponse = RegistrationResponse401 | RegistrationResponseError | RegistrationResponseSuccess; -type RegistrationResponse401 = { +export type RegistrationResponse401 = { completed: string[]; flows: RegistrationFlow[]; params: Record; From a351a185a0ac49dcca97731ca17e547e8c4f671a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 2 Feb 2022 16:41:38 +0530 Subject: [PATCH 090/160] Give proper names --- src/matrix/registration/Registration.ts | 20 +++++++++---------- .../registration/registrationStageFromType.ts | 4 ++-- .../stages/BaseRegistrationStage.ts | 10 +++++----- src/matrix/registration/stages/DummyAuth.ts | 3 +-- src/matrix/registration/stages/TermsAuth.ts | 3 +-- src/matrix/registration/types/types.ts | 7 +++---- 6 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 8b4d54f9..1d44c50d 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -17,32 +17,32 @@ limitations under the License. import type {HomeServerApi} from "../net/HomeServerApi"; import {registrationStageFromType} from "./registrationStageFromType"; import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; -import type {RegistrationDetails, RegistrationFlow, RegistrationResponse401} from "./types/types"; +import type {AccountDetails, RegistrationFlow, RegistrationResponseMoreDataNeeded} from "./types/types"; type FlowSelector = (flows: RegistrationFlow[]) => RegistrationFlow | void; export class Registration { private _hsApi: HomeServerApi; - private _data: RegistrationDetails; + private _accountDetails: AccountDetails; private _flowSelector: FlowSelector; - constructor(hsApi: HomeServerApi, data: RegistrationDetails, flowSelector?: FlowSelector) { + constructor(hsApi: HomeServerApi, accountDetails: AccountDetails, flowSelector?: FlowSelector) { this._hsApi = hsApi; - this._data = data; + this._accountDetails = accountDetails; this._flowSelector = flowSelector ?? (flows => flows.pop()); } async start(): Promise { const response = await this._hsApi.register( - this._data.username, - this._data.password, - this._data.initialDeviceDisplayName, + this._accountDetails.username, + this._accountDetails.password, + this._accountDetails.initialDeviceDisplayName, undefined, - this._data.inhibitLogin).response(); + this._accountDetails.inhibitLogin).response(); return this.parseStagesFromResponse(response); } - parseStagesFromResponse(response: RegistrationResponse401): BaseRegistrationStage { + parseStagesFromResponse(response: RegistrationResponseMoreDataNeeded): BaseRegistrationStage { const { session, params } = response; const flow = this._flowSelector(response.flows); if (!flow) { @@ -55,7 +55,7 @@ export class Registration { if (!stageClass) { throw new Error(`Unknown stage: ${stage}`); } - const registrationStage = new stageClass(this._hsApi, this._data, session, params?.[stage]); + const registrationStage = new stageClass(this._hsApi, this._accountDetails, session, params?.[stage]); if (!firstStage) { firstStage = registrationStage; lastStage = registrationStage; diff --git a/src/matrix/registration/registrationStageFromType.ts b/src/matrix/registration/registrationStageFromType.ts index 9552d049..799451a7 100644 --- a/src/matrix/registration/registrationStageFromType.ts +++ b/src/matrix/registration/registrationStageFromType.ts @@ -16,11 +16,11 @@ limitations under the License. import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; import type {HomeServerApi} from "../net/HomeServerApi"; -import type {RegistrationDetails} from "./types/types"; +import type {AccountDetails, RegistrationParams} from "./types/types"; import {DummyAuth} from "./stages/DummyAuth"; import {TermsAuth} from "./stages/TermsAuth"; -type ClassDerivedFromBaseRegistration = { new(hsApi: HomeServerApi, registrationData: RegistrationDetails, session: string, params?: Record): BaseRegistrationStage } & typeof BaseRegistrationStage; +type ClassDerivedFromBaseRegistration = { new(hsApi: HomeServerApi, registrationData: AccountDetails, session: string, params?: RegistrationParams): BaseRegistrationStage } & typeof BaseRegistrationStage; export function registrationStageFromType(type: string): ClassDerivedFromBaseRegistration | undefined{ switch (type) { diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 7b41379c..88f0bad6 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -15,18 +15,18 @@ limitations under the License. */ import type {HomeServerApi} from "../../net/HomeServerApi"; -import type {RegistrationDetails, RegistrationResponse, AuthenticationData, RegistrationParams} from "../types/types"; +import type {AccountDetails, RegistrationResponse, AuthenticationData, RegistrationParams} from "../types/types"; export abstract class BaseRegistrationStage { protected _hsApi: HomeServerApi; - protected _registrationData: RegistrationDetails; + protected _accountDetails: AccountDetails; protected _session: string; protected _nextStage: BaseRegistrationStage; protected _params?: Record - constructor(hsApi: HomeServerApi, registrationData: RegistrationDetails, session: string, params?: RegistrationParams) { + constructor(hsApi: HomeServerApi, accountDetails: AccountDetails, session: string, params?: RegistrationParams) { this._hsApi = hsApi; - this._registrationData = registrationData; + this._accountDetails = accountDetails; this._session = session; this._params = params; } @@ -52,7 +52,7 @@ export abstract class BaseRegistrationStage { // registration completed successfully return response.user_id; } - else if ("completed" in response && response.completed?.find(c => c === this.type)) { + else if ("completed" in response && response.completed.find(c => c === this.type)) { return this._nextStage; } const error = "error" in response? response.error: "Could not parse response"; diff --git a/src/matrix/registration/stages/DummyAuth.ts b/src/matrix/registration/stages/DummyAuth.ts index 4a5d9bd8..e22409db 100644 --- a/src/matrix/registration/stages/DummyAuth.ts +++ b/src/matrix/registration/stages/DummyAuth.ts @@ -17,9 +17,8 @@ limitations under the License. import {BaseRegistrationStage} from "./BaseRegistrationStage"; export class DummyAuth extends BaseRegistrationStage { - async complete() { - const { username, password, initialDeviceDisplayName, inhibitLogin } = this._registrationData; + const { username, password, initialDeviceDisplayName, inhibitLogin } = this._accountDetails; const response = await this._hsApi.register(username, password, initialDeviceDisplayName, { session: this._session, type: this.type diff --git a/src/matrix/registration/stages/TermsAuth.ts b/src/matrix/registration/stages/TermsAuth.ts index 7d1647b9..e770b13b 100644 --- a/src/matrix/registration/stages/TermsAuth.ts +++ b/src/matrix/registration/stages/TermsAuth.ts @@ -17,9 +17,8 @@ limitations under the License. import {BaseRegistrationStage} from "./BaseRegistrationStage"; export class TermsAuth extends BaseRegistrationStage { - async complete() { - const { username, password, initialDeviceDisplayName, inhibitLogin } = this._registrationData; + const { username, password, initialDeviceDisplayName, inhibitLogin } = this._accountDetails; const response = await this._hsApi.register(username, password, initialDeviceDisplayName, { session: this._session, type: this.type diff --git a/src/matrix/registration/types/types.ts b/src/matrix/registration/types/types.ts index 5661e268..08fc0fc4 100644 --- a/src/matrix/registration/types/types.ts +++ b/src/matrix/registration/types/types.ts @@ -14,16 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -export type RegistrationDetails = { +export type AccountDetails = { username: string | null; password: string; initialDeviceDisplayName: string; inhibitLogin: boolean; } -export type RegistrationResponse = RegistrationResponse401 | RegistrationResponseError | RegistrationResponseSuccess; +export type RegistrationResponse = RegistrationResponseMoreDataNeeded | RegistrationResponseError | RegistrationResponseSuccess; -export type RegistrationResponse401 = { +export type RegistrationResponseMoreDataNeeded = { completed: string[]; flows: RegistrationFlow[]; params: Record; @@ -46,7 +46,6 @@ export type RegistrationFlow = { } /* Types for Registration Stage */ - export type AuthenticationData = { type: string; session: string; From 30cb9f6d15afc5e4ba48a6d697ffbb4e77e56e8d Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 3 Feb 2022 12:01:08 +0530 Subject: [PATCH 091/160] Use includes instead of elaborate find Co-authored-by: Bruno Windels --- src/matrix/net/HomeServerRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/net/HomeServerRequest.ts b/src/matrix/net/HomeServerRequest.ts index cbbb4e90..c3f49834 100644 --- a/src/matrix/net/HomeServerRequest.ts +++ b/src/matrix/net/HomeServerRequest.ts @@ -42,7 +42,7 @@ export class HomeServerRequest implements IHomeServerRequest { this._promise = sourceRequest.response().then(response => { log?.set("status", response.status); // ok? - if (response.status >= 200 && response.status < 300 || options?.allowedErrors?.find(e => e === response.status)) { + if (response.status >= 200 && response.status < 300 || options?.allowedErrors?.includes(response.status)) { log?.finish(); return response.body; } else { From e13040a49e11cc185c62a808853e93162683b199 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 3 Feb 2022 12:04:13 +0530 Subject: [PATCH 092/160] Don't mutate flows --- src/matrix/registration/Registration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 1d44c50d..227d78bc 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -29,7 +29,7 @@ export class Registration { constructor(hsApi: HomeServerApi, accountDetails: AccountDetails, flowSelector?: FlowSelector) { this._hsApi = hsApi; this._accountDetails = accountDetails; - this._flowSelector = flowSelector ?? (flows => flows.pop()); + this._flowSelector = flowSelector ?? (flows => flows[0]); } async start(): Promise { From 7bacbec5e9dcd44d3948d2959896ae585d51b2cf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 3 Feb 2022 12:06:13 +0530 Subject: [PATCH 093/160] Remove type directory --- src/matrix/registration/Registration.ts | 2 +- src/matrix/registration/registrationStageFromType.ts | 2 +- src/matrix/registration/stages/BaseRegistrationStage.ts | 2 +- src/matrix/registration/{types => }/types.ts | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename src/matrix/registration/{types => }/types.ts (100%) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 227d78bc..9c7ef11b 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -17,7 +17,7 @@ limitations under the License. import type {HomeServerApi} from "../net/HomeServerApi"; import {registrationStageFromType} from "./registrationStageFromType"; import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; -import type {AccountDetails, RegistrationFlow, RegistrationResponseMoreDataNeeded} from "./types/types"; +import type {AccountDetails, RegistrationFlow, RegistrationResponseMoreDataNeeded} from "./types"; type FlowSelector = (flows: RegistrationFlow[]) => RegistrationFlow | void; diff --git a/src/matrix/registration/registrationStageFromType.ts b/src/matrix/registration/registrationStageFromType.ts index 799451a7..6b120a6f 100644 --- a/src/matrix/registration/registrationStageFromType.ts +++ b/src/matrix/registration/registrationStageFromType.ts @@ -16,7 +16,7 @@ limitations under the License. import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; import type {HomeServerApi} from "../net/HomeServerApi"; -import type {AccountDetails, RegistrationParams} from "./types/types"; +import type {AccountDetails, RegistrationParams} from "./types"; import {DummyAuth} from "./stages/DummyAuth"; import {TermsAuth} from "./stages/TermsAuth"; diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 88f0bad6..74fe66c7 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -15,7 +15,7 @@ limitations under the License. */ import type {HomeServerApi} from "../../net/HomeServerApi"; -import type {AccountDetails, RegistrationResponse, AuthenticationData, RegistrationParams} from "../types/types"; +import type {AccountDetails, RegistrationResponse, AuthenticationData, RegistrationParams} from "../types"; export abstract class BaseRegistrationStage { protected _hsApi: HomeServerApi; diff --git a/src/matrix/registration/types/types.ts b/src/matrix/registration/types.ts similarity index 100% rename from src/matrix/registration/types/types.ts rename to src/matrix/registration/types.ts From 2aad5546bf73a50df0abd303146b9b3e25aed975 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Thu, 3 Feb 2022 12:07:09 +0530 Subject: [PATCH 094/160] No need for Object.assign here either Co-authored-by: Bruno Windels --- src/matrix/net/HomeServerApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index 7f7dd16c..b9651cee 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -157,7 +157,7 @@ export class HomeServerApi { } register(username: string | null, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin: boolean = true , options: IRequestOptions = {}): IHomeServerRequest { - Object.assign(options, { allowedErrors: [401] }); + options.allowedErrors = [401]; const body: any = { auth, password, From e64f4ad7b2021bd9f340ff8f123daca9d7ab9fa9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 3 Feb 2022 15:10:56 +0530 Subject: [PATCH 095/160] Refactor code - Move all code that does /register to Registration.ts - RegistrationStage only deals with the generation of auth data - Change API so that undefined is returned instead of string when registration is over --- src/matrix/Client.js | 3 +- src/matrix/registration/Registration.ts | 38 ++++++++++++++++++- .../stages/BaseRegistrationStage.ts | 21 +++------- src/matrix/registration/stages/DummyAuth.ts | 11 +++--- src/matrix/registration/stages/TermsAuth.ts | 12 +++--- src/matrix/registration/types.ts | 2 +- 6 files changed, 56 insertions(+), 31 deletions(-) diff --git a/src/matrix/Client.js b/src/matrix/Client.js index 22e686d0..b24c1ec9 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -140,8 +140,7 @@ export class Client { password, initialDeviceDisplayName, }); - let stage = await registration.start(); - return stage; + return registration; } async startWithLogin(loginMethod, {inspectAccountSetup} = {}) { diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 9c7ef11b..0d408c2a 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -17,7 +17,13 @@ limitations under the License. import type {HomeServerApi} from "../net/HomeServerApi"; import {registrationStageFromType} from "./registrationStageFromType"; import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; -import type {AccountDetails, RegistrationFlow, RegistrationResponseMoreDataNeeded} from "./types"; +import type { + AccountDetails, + RegistrationFlow, + RegistrationResponseMoreDataNeeded, + RegistrationResponse, + RegistrationResponseSuccess, +} from "./types"; type FlowSelector = (flows: RegistrationFlow[]) => RegistrationFlow | void; @@ -25,6 +31,7 @@ export class Registration { private _hsApi: HomeServerApi; private _accountDetails: AccountDetails; private _flowSelector: FlowSelector; + private _sessionInfo?: RegistrationResponseSuccess constructor(hsApi: HomeServerApi, accountDetails: AccountDetails, flowSelector?: FlowSelector) { this._hsApi = hsApi; @@ -42,6 +49,18 @@ export class Registration { return this.parseStagesFromResponse(response); } + /** + * Finish a registration stage, return value is: + * - the next stage if this stage was completed successfully + * - undefined if registration is completed + */ + async submitStage(stage: BaseRegistrationStage): Promise { + const auth = stage.generateAuthenticationData(); + const { username, password, initialDeviceDisplayName, inhibitLogin } = this._accountDetails; + const response = await this._hsApi.register(username, password, initialDeviceDisplayName, auth, inhibitLogin).response(); + return this.parseRegistrationResponse(response, stage); + } + parseStagesFromResponse(response: RegistrationResponseMoreDataNeeded): BaseRegistrationStage { const { session, params } = response; const flow = this._flowSelector(response.flows); @@ -66,4 +85,21 @@ export class Registration { } return firstStage!; } + + parseRegistrationResponse(response: RegistrationResponse, currentStage: BaseRegistrationStage) { + if ("user_id" in response) { + // registration completed successfully + this._sessionInfo = response; + return undefined; + } + else if ("completed" in response && response.completed.find(c => c === currentStage.type)) { + return currentStage.nextStage; + } + const error = "error" in response? response.error: "Could not parse response"; + throw new Error(error); + } + + get sessionInfo(): RegistrationResponseSuccess | undefined { + return this._sessionInfo; + } } diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 74fe66c7..dfe71fa5 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -15,7 +15,7 @@ limitations under the License. */ import type {HomeServerApi} from "../../net/HomeServerApi"; -import type {AccountDetails, RegistrationResponse, AuthenticationData, RegistrationParams} from "../types"; +import type {AccountDetails, AuthenticationData, RegistrationParams} from "../types"; export abstract class BaseRegistrationStage { protected _hsApi: HomeServerApi; @@ -37,25 +37,16 @@ export abstract class BaseRegistrationStage { abstract get type(): string; /** - * Finish a registration stage, return value is: - * - the next stage if this stage was completed successfully - * - user-id (string) if registration is completed + * This method should return auth part that must be provided to + * /register endpoint to successfully complete this stage */ - abstract complete(auth?: AuthenticationData): Promise; + abstract generateAuthenticationData(): AuthenticationData; setNextStage(stage: BaseRegistrationStage) { this._nextStage = stage; } - parseResponse(response: RegistrationResponse) { - if ("user_id" in response) { - // registration completed successfully - return response.user_id; - } - else if ("completed" in response && response.completed.find(c => c === this.type)) { - return this._nextStage; - } - const error = "error" in response? response.error: "Could not parse response"; - throw new Error(error); + get nextStage(): BaseRegistrationStage { + return this._nextStage; } } diff --git a/src/matrix/registration/stages/DummyAuth.ts b/src/matrix/registration/stages/DummyAuth.ts index e22409db..b7f0a6ff 100644 --- a/src/matrix/registration/stages/DummyAuth.ts +++ b/src/matrix/registration/stages/DummyAuth.ts @@ -14,16 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {AuthenticationData} from "../types"; import {BaseRegistrationStage} from "./BaseRegistrationStage"; export class DummyAuth extends BaseRegistrationStage { - async complete() { - const { username, password, initialDeviceDisplayName, inhibitLogin } = this._accountDetails; - const response = await this._hsApi.register(username, password, initialDeviceDisplayName, { + generateAuthenticationData(): AuthenticationData { + return { session: this._session, - type: this.type - }, inhibitLogin).response(); - return this.parseResponse(response); + type: this.type, + }; } get type(): string { diff --git a/src/matrix/registration/stages/TermsAuth.ts b/src/matrix/registration/stages/TermsAuth.ts index e770b13b..bf54dd4d 100644 --- a/src/matrix/registration/stages/TermsAuth.ts +++ b/src/matrix/registration/stages/TermsAuth.ts @@ -14,16 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {AuthenticationData} from "../types"; import {BaseRegistrationStage} from "./BaseRegistrationStage"; export class TermsAuth extends BaseRegistrationStage { - async complete() { - const { username, password, initialDeviceDisplayName, inhibitLogin } = this._accountDetails; - const response = await this._hsApi.register(username, password, initialDeviceDisplayName, { + generateAuthenticationData(): AuthenticationData { + return { session: this._session, - type: this.type - }, inhibitLogin).response(); - return this.parseResponse(response); + type: this.type, + // No other auth data needed for m.login.terms + }; } get type(): string { diff --git a/src/matrix/registration/types.ts b/src/matrix/registration/types.ts index 08fc0fc4..aa319d43 100644 --- a/src/matrix/registration/types.ts +++ b/src/matrix/registration/types.ts @@ -35,7 +35,7 @@ type RegistrationResponseError = { error: string; } -type RegistrationResponseSuccess = { +export type RegistrationResponseSuccess = { user_id: string; device_id: string; access_token?: string; From c4894f2c240b89e6d85f4095b90e2e5969b53d49 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 3 Feb 2022 18:07:45 +0530 Subject: [PATCH 096/160] completed is not always present --- src/matrix/registration/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/registration/types.ts b/src/matrix/registration/types.ts index aa319d43..d79fd33a 100644 --- a/src/matrix/registration/types.ts +++ b/src/matrix/registration/types.ts @@ -24,7 +24,7 @@ export type AccountDetails = { export type RegistrationResponse = RegistrationResponseMoreDataNeeded | RegistrationResponseError | RegistrationResponseSuccess; export type RegistrationResponseMoreDataNeeded = { - completed: string[]; + completed?: string[]; flows: RegistrationFlow[]; params: Record; session: string; From 0ad0ecfcc23dd27527f7f6ca0fa7019f044e70da Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 3 Feb 2022 18:08:19 +0530 Subject: [PATCH 097/160] Check response code instead of existence of props --- src/matrix/net/HomeServerRequest.ts | 6 +++++ src/matrix/registration/Registration.ts | 33 ++++++++++++++++--------- src/matrix/registration/types.ts | 3 +++ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/matrix/net/HomeServerRequest.ts b/src/matrix/net/HomeServerRequest.ts index c3f49834..066d5796 100644 --- a/src/matrix/net/HomeServerRequest.ts +++ b/src/matrix/net/HomeServerRequest.ts @@ -23,6 +23,7 @@ import type {IRequestOptions} from "../../platform/types/types.js"; export interface IHomeServerRequest { abort(): void; response(): Promise; + responseCode(): Promise; } export class HomeServerRequest implements IHomeServerRequest { @@ -110,6 +111,11 @@ export class HomeServerRequest implements IHomeServerRequest { response(): Promise { return this._promise; } + + async responseCode(): Promise { + const response = await this._sourceRequest.response(); + return response.status; + } } import {Request as MockRequest} from "../../mocks/Request.js"; diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 0d408c2a..9b2c0684 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -57,8 +57,11 @@ export class Registration { async submitStage(stage: BaseRegistrationStage): Promise { const auth = stage.generateAuthenticationData(); const { username, password, initialDeviceDisplayName, inhibitLogin } = this._accountDetails; - const response = await this._hsApi.register(username, password, initialDeviceDisplayName, auth, inhibitLogin).response(); - return this.parseRegistrationResponse(response, stage); + const request = this._hsApi.register(username, password, initialDeviceDisplayName, auth, inhibitLogin); + const response = await request.response(); + const status = await request.responseCode(); + const registrationResponse: RegistrationResponse = { ...response, status }; + return this.parseRegistrationResponse(registrationResponse, stage); } parseStagesFromResponse(response: RegistrationResponseMoreDataNeeded): BaseRegistrationStage { @@ -86,17 +89,23 @@ export class Registration { return firstStage!; } - parseRegistrationResponse(response: RegistrationResponse, currentStage: BaseRegistrationStage) { - if ("user_id" in response) { - // registration completed successfully - this._sessionInfo = response; - return undefined; + async parseRegistrationResponse(response: RegistrationResponse, currentStage: BaseRegistrationStage) { + switch (response.status) { + case 200: + this._sessionInfo = response; + return undefined; + case 401: + if (response.completed?.includes(currentStage.type)) { + return currentStage.nextStage; + } + else { + throw new Error("This stage could not be completed!"); + } + case 400: + default: + const error = "error" in response? response.error: "Could not parse response"; + throw new Error(error); } - else if ("completed" in response && response.completed.find(c => c === currentStage.type)) { - return currentStage.nextStage; - } - const error = "error" in response? response.error: "Could not parse response"; - throw new Error(error); } get sessionInfo(): RegistrationResponseSuccess | undefined { diff --git a/src/matrix/registration/types.ts b/src/matrix/registration/types.ts index d79fd33a..77d051af 100644 --- a/src/matrix/registration/types.ts +++ b/src/matrix/registration/types.ts @@ -28,17 +28,20 @@ export type RegistrationResponseMoreDataNeeded = { flows: RegistrationFlow[]; params: Record; session: string; + status: 401; } type RegistrationResponseError = { errcode: string; error: string; + status: 400; } export type RegistrationResponseSuccess = { user_id: string; device_id: string; access_token?: string; + status: 200; } export type RegistrationFlow = { From 8a3c0afba6aa263819145445f82e5da4bcecb411 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 3 Feb 2022 18:42:02 +0530 Subject: [PATCH 098/160] Fix incorrect types --- src/matrix/net/HomeServerApi.ts | 82 ++++++++++++++++------------- src/matrix/net/HomeServerRequest.ts | 8 ++- src/platform/types/types.ts | 3 -- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index b9651cee..88ff32d9 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -20,7 +20,7 @@ import {HomeServerRequest} from "./HomeServerRequest"; import type {IHomeServerRequest} from "./HomeServerRequest"; import type {Reconnector} from "./Reconnector"; import type {EncodedBody} from "./common"; -import type {IRequestOptions, RequestFunction} from "../../platform/types/types"; +import type {RequestFunction} from "../../platform/types/types"; import type {ILogItem} from "../../logging/types"; type RequestMethod = "POST" | "GET" | "PUT"; @@ -36,6 +36,14 @@ type Options = { reconnector: Reconnector; }; +type BaseRequestOptions = { + log?: ILogItem; + allowedErrors?: number[]; + uploadProgress?: (loadedBytes: number) => void; + timeout?: number; + prefix?: string; +}; + export class HomeServerApi { private readonly _homeserver: string; private readonly _accessToken: string; @@ -55,7 +63,7 @@ export class HomeServerApi { return this._homeserver + prefix + csPath; } - private _baseRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: IRequestOptions, accessToken?: string): IHomeServerRequest { + private _baseRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: BaseRequestOptions, accessToken?: string): IHomeServerRequest { const queryString = encodeQueryParams(queryParams); url = `${url}?${queryString}`; let encodedBody: EncodedBody["body"]; @@ -97,27 +105,27 @@ export class HomeServerApi { return hsRequest; } - private _unauthedRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: IRequestOptions): IHomeServerRequest { + private _unauthedRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._baseRequest(method, url, queryParams, body, options); } - private _authedRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: IRequestOptions): IHomeServerRequest { + private _authedRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._baseRequest(method, url, queryParams, body, options, this._accessToken); } - private _post(csPath: string, queryParams: Record, body: Record, options?: IRequestOptions): IHomeServerRequest { + private _post(csPath: string, queryParams: Record, body: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._authedRequest("POST", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); } - private _put(csPath: string, queryParams: Record, body?: Record, options?: IRequestOptions): IHomeServerRequest { + private _put(csPath: string, queryParams: Record, body?: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._authedRequest("PUT", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); } - private _get(csPath: string, queryParams?: Record, body?: Record, options?: IRequestOptions): IHomeServerRequest { + private _get(csPath: string, queryParams?: Record, body?: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._authedRequest("GET", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); } - sync(since: string, filter: string, timeout: number, options?: IRequestOptions): IHomeServerRequest { + sync(since: string, filter: string, timeout: number, options?: BaseRequestOptions): IHomeServerRequest { return this._get("/sync", {since, timeout, filter}, undefined, options); } @@ -126,29 +134,29 @@ export class HomeServerApi { } // params is from, dir and optionally to, limit, filter. - messages(roomId: string, params: Record, options?: IRequestOptions): IHomeServerRequest { + messages(roomId: string, params: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, undefined, options); } // params is at, membership and not_membership - members(roomId: string, params: Record, options?: IRequestOptions): IHomeServerRequest { + members(roomId: string, params: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, undefined, options); } - send(roomId: string, eventType: string, txnId: string, content: Record, options?: IRequestOptions): IHomeServerRequest { + send(roomId: string, eventType: string, txnId: string, content: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options); } - redact(roomId: string, eventId: string, txnId: string, content: Record, options?: IRequestOptions): IHomeServerRequest { + redact(roomId: string, eventId: string, txnId: string, content: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._put(`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`, {}, content, options); } - receipt(roomId: string, receiptType: string, eventId: string, options?: IRequestOptions): IHomeServerRequest { + receipt(roomId: string, receiptType: string, eventId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/rooms/${encodeURIComponent(roomId)}/receipt/${encodeURIComponent(receiptType)}/${encodeURIComponent(eventId)}`, {}, {}, options); } - state(roomId: string, eventType: string, stateKey: string, options?: IRequestOptions): IHomeServerRequest { + state(roomId: string, eventType: string, stateKey: string, options?: BaseRequestOptions): IHomeServerRequest { return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, undefined, options); } @@ -156,7 +164,7 @@ export class HomeServerApi { return this._unauthedRequest("GET", this._url("/login")); } - register(username: string | null, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin: boolean = true , options: IRequestOptions = {}): IHomeServerRequest { + register(username: string | null, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin: boolean = true , options: BaseRequestOptions = {}): IHomeServerRequest { options.allowedErrors = [401]; const body: any = { auth, @@ -171,7 +179,7 @@ export class HomeServerApi { return this._unauthedRequest( "POST", this._url("/register", CS_V3_PREFIX), undefined, body, options); } - passwordLogin(username: string, password: string, initialDeviceDisplayName: string, options?: IRequestOptions): IHomeServerRequest { + passwordLogin(username: string, password: string, initialDeviceDisplayName: string, options?: BaseRequestOptions): IHomeServerRequest { return this._unauthedRequest("POST", this._url("/login"), undefined, { "type": "m.login.password", "identifier": { @@ -183,7 +191,7 @@ export class HomeServerApi { }, options); } - tokenLogin(loginToken: string, txnId: string, initialDeviceDisplayName: string, options?: IRequestOptions): IHomeServerRequest { + tokenLogin(loginToken: string, txnId: string, initialDeviceDisplayName: string, options?: BaseRequestOptions): IHomeServerRequest { return this._unauthedRequest("POST", this._url("/login"), undefined, { "type": "m.login.token", "identifier": { @@ -195,15 +203,15 @@ export class HomeServerApi { }, options); } - createFilter(userId: string, filter: Record, options?: IRequestOptions): IHomeServerRequest { + createFilter(userId: string, filter: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/user/${encodeURIComponent(userId)}/filter`, {}, filter, options); } - versions(options?: IRequestOptions): IHomeServerRequest { + versions(options?: BaseRequestOptions): IHomeServerRequest { return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, undefined, undefined, options); } - uploadKeys(dehydratedDeviceId: string, payload: Record, options?: IRequestOptions): IHomeServerRequest { + uploadKeys(dehydratedDeviceId: string, payload: Record, options?: BaseRequestOptions): IHomeServerRequest { let path = "/keys/upload"; if (dehydratedDeviceId) { path = path + `/${encodeURIComponent(dehydratedDeviceId)}`; @@ -211,19 +219,19 @@ export class HomeServerApi { return this._post(path, {}, payload, options); } - queryKeys(queryRequest: Record, options?: IRequestOptions): IHomeServerRequest { + queryKeys(queryRequest: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._post("/keys/query", {}, queryRequest, options); } - claimKeys(payload: Record, options?: IRequestOptions): IHomeServerRequest { + claimKeys(payload: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._post("/keys/claim", {}, payload, options); } - sendToDevice(type: string, payload: Record, txnId: string, options?: IRequestOptions): IHomeServerRequest { + sendToDevice(type: string, payload: Record, txnId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, {}, payload, options); } - roomKeysVersion(version?: string, options?: IRequestOptions): IHomeServerRequest { + roomKeysVersion(version?: string, options?: BaseRequestOptions): IHomeServerRequest { let versionPart = ""; if (version) { versionPart = `/${encodeURIComponent(version)}`; @@ -231,57 +239,57 @@ export class HomeServerApi { return this._get(`/room_keys/version${versionPart}`, undefined, undefined, options); } - roomKeyForRoomAndSession(version: string, roomId: string, sessionId: string, options?: IRequestOptions): IHomeServerRequest { + roomKeyForRoomAndSession(version: string, roomId: string, sessionId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, undefined, options); } - uploadRoomKeysToBackup(version: string, payload: Record, options?: IRequestOptions): IHomeServerRequest { + uploadRoomKeysToBackup(version: string, payload: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._put(`/room_keys/keys`, {version}, payload, options); } - uploadAttachment(blob: Blob, filename: string, options?: IRequestOptions): IHomeServerRequest { + uploadAttachment(blob: Blob, filename: string, options?: BaseRequestOptions): IHomeServerRequest { return this._authedRequest("POST", `${this._homeserver}/_matrix/media/r0/upload`, {filename}, blob, options); } - setPusher(pusher: Record, options?: IRequestOptions): IHomeServerRequest { + setPusher(pusher: Record, options?: BaseRequestOptions): IHomeServerRequest { return this._post("/pushers/set", {}, pusher, options); } - getPushers(options?: IRequestOptions): IHomeServerRequest { + getPushers(options?: BaseRequestOptions): IHomeServerRequest { return this._get("/pushers", undefined, undefined, options); } - join(roomId: string, options?: IRequestOptions): IHomeServerRequest { + join(roomId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/rooms/${encodeURIComponent(roomId)}/join`, {}, {}, options); } - joinIdOrAlias(roomIdOrAlias: string, options?: IRequestOptions): IHomeServerRequest { + joinIdOrAlias(roomIdOrAlias: string, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`, {}, {}, options); } - leave(roomId: string, options?: IRequestOptions): IHomeServerRequest { + leave(roomId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/rooms/${encodeURIComponent(roomId)}/leave`, {}, {}, options); } - forget(roomId: string, options?: IRequestOptions): IHomeServerRequest { + forget(roomId: string, options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/rooms/${encodeURIComponent(roomId)}/forget`, {}, {}, options); } - logout(options?: IRequestOptions): IHomeServerRequest { + logout(options?: BaseRequestOptions): IHomeServerRequest { return this._post(`/logout`, {}, {}, options); } - getDehydratedDevice(options: IRequestOptions): IHomeServerRequest { + getDehydratedDevice(options: BaseRequestOptions): IHomeServerRequest { options.prefix = DEHYDRATION_PREFIX; return this._get(`/dehydrated_device`, undefined, undefined, options); } - createDehydratedDevice(payload: Record, options: IRequestOptions): IHomeServerRequest { + createDehydratedDevice(payload: Record, options: BaseRequestOptions): IHomeServerRequest { options.prefix = DEHYDRATION_PREFIX; return this._put(`/dehydrated_device`, {}, payload, options); } - claimDehydratedDevice(deviceId: string, options: IRequestOptions): IHomeServerRequest { + claimDehydratedDevice(deviceId: string, options: BaseRequestOptions): IHomeServerRequest { options.prefix = DEHYDRATION_PREFIX; return this._post(`/dehydrated_device/claim`, {}, {device_id: deviceId}, options); } diff --git a/src/matrix/net/HomeServerRequest.ts b/src/matrix/net/HomeServerRequest.ts index 066d5796..ee4ad2ba 100644 --- a/src/matrix/net/HomeServerRequest.ts +++ b/src/matrix/net/HomeServerRequest.ts @@ -18,7 +18,6 @@ limitations under the License. import {HomeServerError, ConnectionError} from "../error.js"; import type {RequestResult} from "../../platform/web/dom/request/fetch.js"; import type {ILogItem} from "../../logging/types"; -import type {IRequestOptions} from "../../platform/types/types.js"; export interface IHomeServerRequest { abort(): void; @@ -26,13 +25,18 @@ export interface IHomeServerRequest { responseCode(): Promise; } +type HomeServerRequestOptions = { + log?: ILogItem; + allowedErrors?: number[]; +}; + export class HomeServerRequest implements IHomeServerRequest { private readonly _log?: ILogItem; private _sourceRequest?: RequestResult; // as we add types for expected responses from hs, this could be a generic class instead private readonly _promise: Promise; - constructor(method: string, url: string, sourceRequest: RequestResult, options?: IRequestOptions) { + constructor(method: string, url: string, sourceRequest: RequestResult, options?: HomeServerRequestOptions) { let log: ILogItem | undefined; if (options?.log) { const parent = options?.log; diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts index 0ad569b4..cc57d56f 100644 --- a/src/platform/types/types.ts +++ b/src/platform/types/types.ts @@ -24,11 +24,8 @@ export interface IRequestOptions { body?: EncodedBody; headers?: Map; cache?: boolean; - log?: ILogItem; - prefix?: string; method?: string; format?: string; - allowedErrors?: number[]; } export type RequestFunction = (url: string, options: IRequestOptions) => RequestResult; From 89a97537b0d081ef56a8e06b5418b6ff9cd58943 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 3 Feb 2022 19:37:14 +0530 Subject: [PATCH 099/160] Make methods private + some props readonly --- src/matrix/registration/Registration.ts | 10 +++++----- .../registration/stages/BaseRegistrationStage.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 9b2c0684..d50cbca5 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -28,9 +28,9 @@ import type { type FlowSelector = (flows: RegistrationFlow[]) => RegistrationFlow | void; export class Registration { - private _hsApi: HomeServerApi; - private _accountDetails: AccountDetails; - private _flowSelector: FlowSelector; + private readonly _hsApi: HomeServerApi; + private readonly _accountDetails: AccountDetails; + private readonly _flowSelector: FlowSelector; private _sessionInfo?: RegistrationResponseSuccess constructor(hsApi: HomeServerApi, accountDetails: AccountDetails, flowSelector?: FlowSelector) { @@ -64,7 +64,7 @@ export class Registration { return this.parseRegistrationResponse(registrationResponse, stage); } - parseStagesFromResponse(response: RegistrationResponseMoreDataNeeded): BaseRegistrationStage { + private parseStagesFromResponse(response: RegistrationResponseMoreDataNeeded): BaseRegistrationStage { const { session, params } = response; const flow = this._flowSelector(response.flows); if (!flow) { @@ -89,7 +89,7 @@ export class Registration { return firstStage!; } - async parseRegistrationResponse(response: RegistrationResponse, currentStage: BaseRegistrationStage) { + private async parseRegistrationResponse(response: RegistrationResponse, currentStage: BaseRegistrationStage) { switch (response.status) { case 200: this._sessionInfo = response; diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index dfe71fa5..964cc75d 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -18,11 +18,11 @@ import type {HomeServerApi} from "../../net/HomeServerApi"; import type {AccountDetails, AuthenticationData, RegistrationParams} from "../types"; export abstract class BaseRegistrationStage { - protected _hsApi: HomeServerApi; - protected _accountDetails: AccountDetails; + protected readonly _hsApi: HomeServerApi; + protected readonly _accountDetails: AccountDetails; protected _session: string; protected _nextStage: BaseRegistrationStage; - protected _params?: Record + protected readonly _params?: Record constructor(hsApi: HomeServerApi, accountDetails: AccountDetails, session: string, params?: RegistrationParams) { this._hsApi = hsApi; From b6e1d4a7d5a3f243e2c50951407febe155f92e89 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 3 Feb 2022 19:37:34 +0530 Subject: [PATCH 100/160] Implement responseCode() --- src/matrix/net/RequestScheduler.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/matrix/net/RequestScheduler.ts b/src/matrix/net/RequestScheduler.ts index 45405da3..e36a3541 100644 --- a/src/matrix/net/RequestScheduler.ts +++ b/src/matrix/net/RequestScheduler.ts @@ -50,6 +50,13 @@ class Request implements IHomeServerRequest { response(): Promise { return this._responsePromise; } + + async responseCode(): Promise { + let resolve; + const promise: Promise = new Promise(r => resolve = r); + this.requestResult?.responseCode().then(code => resolve(code)); + return promise; + } } class HomeServerApiWrapper { From bc09ede09f6e9529ae367e864acb9617347cffa4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 3 Feb 2022 17:57:35 +0100 Subject: [PATCH 101/160] WIP --- src/domain/session/RoomViewModelObservable.js | 24 ++++-- src/domain/session/SessionViewModel.js | 19 +++-- .../session/leftpanel/LeftPanelViewModel.js | 19 +++-- .../RoomBeingCreatedTileViewModel.js | 53 +++++++++++++ .../rightpanel/MemberDetailsViewModel.js | 7 ++ .../session/rightpanel/RightPanelViewModel.js | 9 ++- src/domain/session/room/README.md | 14 +++- .../session/room/RoomBeingCreatedViewModel.js | 65 +++++++++++++++ src/matrix/Session.js | 79 +++++++++++++------ src/matrix/room/Room.js | 1 + src/matrix/room/RoomStatus.js | 61 -------------- src/matrix/room/RoomStatus.ts | 24 ++++++ src/matrix/room/create.ts | 61 ++++++++++---- src/platform/web/ui/session/RoomGridView.js | 5 +- src/platform/web/ui/session/SessionView.js | 3 + .../ui/session/leftpanel/InviteTileView.js | 12 ++- .../web/ui/session/leftpanel/LeftPanelView.js | 2 +- .../session/rightpanel/MemberDetailsView.js | 3 +- .../web/ui/session/room/MessageComposer.js | 2 +- .../ui/session/room/RoomBeingCreatedView.js | 25 ++++++ 20 files changed, 360 insertions(+), 128 deletions(-) create mode 100644 src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js create mode 100644 src/domain/session/room/RoomBeingCreatedViewModel.js delete mode 100644 src/matrix/room/RoomStatus.js create mode 100644 src/matrix/room/RoomStatus.ts create mode 100644 src/platform/web/ui/session/room/RoomBeingCreatedView.js diff --git a/src/domain/session/RoomViewModelObservable.js b/src/domain/session/RoomViewModelObservable.js index 8d576a98..c16aaf83 100644 --- a/src/domain/session/RoomViewModelObservable.js +++ b/src/domain/session/RoomViewModelObservable.js @@ -15,6 +15,7 @@ limitations under the License. */ import {ObservableValue} from "../../observable/ObservableValue"; +import {RoomStatus} from "../../matrix/room/RoomStatus"; /** Depending on the status of a room (invited, joined, archived, or none), @@ -34,11 +35,11 @@ the now transferred child view model. This is also why there is an explicit initialize method, see comment there. */ export class RoomViewModelObservable extends ObservableValue { - constructor(sessionViewModel, roomId) { + constructor(sessionViewModel, roomIdOrLocalId) { super(null); this._statusSubscription = null; this._sessionViewModel = sessionViewModel; - this.id = roomId; + this.id = roomIdOrLocalId; } /** @@ -59,11 +60,24 @@ export class RoomViewModelObservable extends ObservableValue { } async _statusToViewModel(status) { - if (status.invited) { + console.log("RoomViewModelObservable received status", status); + if (status & RoomStatus.Replaced) { + console.log("replaced!"); + if (status & RoomStatus.BeingCreated) { + const {session} = this._sessionViewModel._client; + const roomBeingCreated = session.roomsBeingCreated.get(this.id); + console.log("new id is", roomBeingCreated.roomId); + this._sessionViewModel.navigation.push("room", roomBeingCreated.roomId); + } else { + throw new Error("Don't know how to replace a room with this status: " + (status ^ RoomStatus.Replaced)); + } + } else if (status & RoomStatus.BeingCreated) { + return this._sessionViewModel._createRoomBeingCreatedViewModel(this.id); + } else if (status & RoomStatus.Invited) { return this._sessionViewModel._createInviteViewModel(this.id); - } else if (status.joined) { + } else if (status & RoomStatus.Joined) { return this._sessionViewModel._createRoomViewModel(this.id); - } else if (status.archived) { + } else if (status & RoomStatus.Archived) { return await this._sessionViewModel._createArchivedRoomViewModel(this.id); } else { return this._sessionViewModel._createUnknownRoomViewModel(this.id); diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 822a4d31..11126ca9 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -19,6 +19,7 @@ import {LeftPanelViewModel} from "./leftpanel/LeftPanelViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js"; import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js"; import {InviteViewModel} from "./room/InviteViewModel.js"; +import {RoomBeingCreatedViewModel} from "./room/RoomBeingCreatedViewModel.js"; import {LightboxViewModel} from "./room/LightboxViewModel.js"; import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {RoomGridViewModel} from "./RoomGridViewModel.js"; @@ -37,10 +38,7 @@ export class SessionViewModel extends ViewModel { reconnector: client.reconnector, session: client.session, }))); - this._leftPanelViewModel = this.track(new LeftPanelViewModel(this.childOptions({ - invites: this._client.session.invites, - rooms: this._client.session.rooms - }))); + this._leftPanelViewModel = this.track(new LeftPanelViewModel(this.childOptions({session: this._client.session}))); this._settingsViewModel = null; this._roomViewModelObservable = null; this._gridViewModel = null; @@ -200,6 +198,17 @@ export class SessionViewModel extends ViewModel { return null; } + _createRoomBeingCreatedViewModel(localId) { + const roomBeingCreated = this._client.session.roomsBeingCreated.get(localId); + if (roomBeingCreated) { + return new RoomBeingCreatedViewModel(this.childOptions({ + roomBeingCreated, + mediaRepository: this._client.session.mediaRepository, + })); + } + return null; + } + _updateRoom(roomId) { // opening a room and already open? if (this._roomViewModelObservable?.id === roomId) { @@ -263,7 +272,7 @@ export class SessionViewModel extends ViewModel { const enable = !!this.navigation.path.get("right-panel")?.value; if (enable) { const room = this._roomFromNavigation(); - this._rightPanelViewModel = this.track(new RightPanelViewModel(this.childOptions({room}))); + this._rightPanelViewModel = this.track(new RightPanelViewModel(this.childOptions({room, session: this._client.session}))); } this.emitChange("rightPanelViewModel"); } diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 59b68ca1..d85e5943 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -18,6 +18,7 @@ limitations under the License. import {ViewModel} from "../../ViewModel.js"; import {RoomTileViewModel} from "./RoomTileViewModel.js"; import {InviteTileViewModel} from "./InviteTileViewModel.js"; +import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js"; import {RoomFilter} from "./RoomFilter.js"; import {ApplyMap} from "../../../observable/map/ApplyMap.js"; import {addPanelIfNeeded} from "../../navigation/index.js"; @@ -25,8 +26,8 @@ import {addPanelIfNeeded} from "../../navigation/index.js"; export class LeftPanelViewModel extends ViewModel { constructor(options) { super(options); - const {rooms, invites} = options; - this._tileViewModelsMap = this._mapTileViewModels(rooms, invites); + const {session} = options; + this._tileViewModelsMap = this._mapTileViewModels(session.roomsBeingCreated, session.invites, session.rooms); this._tileViewModelsFilterMap = new ApplyMap(this._tileViewModelsMap); this._tileViewModels = this._tileViewModelsFilterMap.sortValues((a, b) => a.compare(b)); this._currentTileVM = null; @@ -35,16 +36,18 @@ export class LeftPanelViewModel extends ViewModel { this._settingsUrl = this.urlCreator.urlForSegment("settings"); } - _mapTileViewModels(rooms, invites) { + _mapTileViewModels(roomsBeingCreated, invites, rooms) { // join is not commutative, invites will take precedence over rooms - return invites.join(rooms).mapValues((roomOrInvite, emitChange) => { + return roomsBeingCreated.join(invites).join(rooms).mapValues((item, emitChange) => { let vm; - if (roomOrInvite.isInvite) { - vm = new InviteTileViewModel(this.childOptions({invite: roomOrInvite, emitChange})); + if (item.isBeingCreated) { + vm = new RoomBeingCreatedTileViewModel(this.childOptions({roomBeingCreated: item, emitChange})); + } else if (item.isInvite) { + vm = new InviteTileViewModel(this.childOptions({invite: item, emitChange})); } else { - vm = new RoomTileViewModel(this.childOptions({room: roomOrInvite, emitChange})); + vm = new RoomTileViewModel(this.childOptions({room: item, emitChange})); } - const isOpen = this.navigation.path.get("room")?.value === roomOrInvite.id; + const isOpen = this.navigation.path.get("room")?.value === item.id; if (isOpen) { vm.open(); this._updateCurrentVM(vm); diff --git a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js new file mode 100644 index 00000000..8c629892 --- /dev/null +++ b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js @@ -0,0 +1,53 @@ +/* +Copyright 2020 Bruno Windels +Copyright 2020, 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +import {BaseTileViewModel} from "./BaseTileViewModel.js"; + +export class RoomBeingCreatedTileViewModel extends BaseTileViewModel { + constructor(options) { + super(options); + const {roomBeingCreated} = options; + this._roomBeingCreated = roomBeingCreated; + this._url = this.urlCreator.openRoomActionUrl(this._roomBeingCreated.localId); + } + + get busy() { return true; } + + get kind() { + return "roomBeingCreated"; + } + + get url() { + return this._url; + } + + compare(other) { + const parentComparison = super.compare(other); + if (parentComparison !== 0) { + return parentComparison; + } + return other._roomBeingCreated.name.localeCompare(this._roomBeingCreated.name); + } + + get name() { + return this._roomBeingCreated.name; + } + + get _avatarSource() { + return this._roomBeingCreated; + } +} diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index 0303e05d..6effb90c 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -15,6 +15,7 @@ limitations under the License. */ import {ViewModel} from "../../ViewModel.js"; +import {RoomType} from "../../../matrix/room/create"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; export class MemberDetailsViewModel extends ViewModel { @@ -25,6 +26,7 @@ export class MemberDetailsViewModel extends ViewModel { this._member = this._observableMember.get(); this._isEncrypted = options.isEncrypted; this._powerLevelsObservable = options.powerLevelsObservable; + this._session = options.session; this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange())); this.track(this._observableMember.subscribe( () => this._onMemberChange())); } @@ -79,4 +81,9 @@ export class MemberDetailsViewModel extends ViewModel { get linkToUser() { return `https://matrix.to/#/${this._member.userId}`; } + + async openDirectMessage() { + const localId = await this._session.createRoom(RoomType.DirectMessage, undefined, undefined, undefined, [this.userId]); + this.navigation.push("room", localId); + } } diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index 670df868..3cfe378b 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -23,6 +23,7 @@ export class RightPanelViewModel extends ViewModel { constructor(options) { super(options); this._room = options.room; + this._session = options.session; this._members = null; this._setupNavigation(); } @@ -48,7 +49,13 @@ export class RightPanelViewModel extends ViewModel { } const isEncrypted = this._room.isEncrypted; const powerLevelsObservable = await this._room.observePowerLevels(); - return {observableMember, isEncrypted, powerLevelsObservable, mediaRepository: this._room.mediaRepository}; + return { + observableMember, + isEncrypted, + powerLevelsObservable, + mediaRepository: this._room.mediaRepository, + session: this._session + }; } _setupNavigation() { diff --git a/src/domain/session/room/README.md b/src/domain/session/room/README.md index adb673eb..dbbe4dca 100644 --- a/src/domain/session/room/README.md +++ b/src/domain/session/room/README.md @@ -1,9 +1,17 @@ # "Room" view models -InviteViewModel and RoomViewModel are interchangebly used as "room view model": - - SessionViewModel.roomViewModel can be an instance of either - - RoomGridViewModel.roomViewModelAt(i) can return an instance of either +InviteViewModel, RoomViewModel and RoomBeingCreatedViewModel are interchangebly used as "room view model": + - SessionViewModel.roomViewModel can be an instance of any + - RoomGridViewModel.roomViewModelAt(i) can return an instance of any This is because they are accessed by the same url and need to transition into each other, in these two locations. Having two methods, especially in RoomGridViewModel would have been more cumbersome, even though this is not in line with how different view models are exposed in SessionViewModel. They share an `id` and `kind` property, the latter can be used to differentiate them from the view, and a `focus` method. +Once we convert this folder to typescript, we should use this interface for all the view models: +```ts +interface IGridItemViewModel { + id: string; + kind: string; + focus(); +} +``` diff --git a/src/domain/session/room/RoomBeingCreatedViewModel.js b/src/domain/session/room/RoomBeingCreatedViewModel.js new file mode 100644 index 00000000..eebbb668 --- /dev/null +++ b/src/domain/session/room/RoomBeingCreatedViewModel.js @@ -0,0 +1,65 @@ +/* +Copyright 2020 Bruno Windels +Copyright 2020, 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; +import {ViewModel} from "../../ViewModel.js"; + +export class RoomBeingCreatedViewModel extends ViewModel { + constructor(options) { + super(options); + const {roomBeingCreated, mediaRepository} = options; + this._roomBeingCreated = roomBeingCreated; + this._mediaRepository = mediaRepository; + this._onRoomChange = this._onRoomChange.bind(this); + this._closeUrl = this.urlCreator.urlUntilSegment("session"); + this._roomBeingCreated.on("change", this._onRoomChange); + } + + get kind() { return "roomBeingCreated"; } + get closeUrl() { return this._closeUrl; } + get name() { return this._roomBeingCreated.name; } + get id() { return this._roomBeingCreated.localId; } + get isEncrypted() { return this._roomBeingCreated.isEncrypted; } + + get avatarLetter() { + return avatarInitials(this.name); + } + + get avatarColorNumber() { + return getIdentifierColorNumber(this._roomBeingCreated.avatarColorId); + } + + avatarUrl(size) { + return getAvatarHttpUrl(this._roomBeingCreated.avatarUrl, size, this.platform, this._mediaRepository); + } + + get avatarTitle() { + return this.name; + } + + focus() {} + + _onRoomChange() { + this.emitChange(); + } + + dispose() { + super.dispose(); + this._roomBeingCreated.off("change", this._onRoomChange); + } +} + diff --git a/src/matrix/Session.js b/src/matrix/Session.js index f8d42960..49ae0f9c 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -17,7 +17,7 @@ limitations under the License. import {Room} from "./room/Room.js"; import {ArchivedRoom} from "./room/ArchivedRoom.js"; -import {RoomStatus} from "./room/RoomStatus.js"; +import {RoomStatus} from "./room/RoomStatus"; import {RoomBeingCreated} from "./room/create"; import {Invite} from "./room/Invite.js"; import {Pusher} from "./push/Pusher"; @@ -64,6 +64,12 @@ export class Session { this._activeArchivedRooms = new Map(); this._invites = new ObservableMap(); this._inviteUpdateCallback = (invite, params) => this._invites.update(invite.id, params); + this._roomsBeingCreatedUpdateCallback = (rbc, params) => { + this._roomsBeingCreated.update(rbc.localId, params); + if (rbc.roomId && !!this.rooms.get(rbc.roomId)) { + this._tryReplaceRoomBeingCreated(rbc.roomId); + } + }; this._roomsBeingCreated = new ObservableMap(); this._user = new User(sessionInfo.userId); this._deviceMessageHandler = new DeviceMessageHandler({storage}); @@ -586,14 +592,16 @@ export class Session { return this._roomsBeingCreated; } - createRoom(type, isEncrypted, explicitName, topic, invites) { - const localId = `room-being-created-${this.platform.random()}`; - const roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, explicitName, topic, invites); - this._roomsBeingCreated.set(localId, roomBeingCreated); - this._platform.logger.runDetached("create room", async log => { - roomBeingCreated.start(this._hsApi, log); + createRoom(type, isEncrypted, explicitName, topic, invites, log = undefined) { + return this._platform.logger.wrapOrRun(log, "create room", log => { + const localId = `room-being-created-${this._platform.random()}`; + const roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, explicitName, topic, invites, this._roomsBeingCreatedUpdateCallback, this._mediaRepository, log); + this._roomsBeingCreated.set(localId, roomBeingCreated); + log.wrapDetached("create room network", async log => { + roomBeingCreated.start(this._hsApi, log); + }); + return localId; }); - return localId; } async obtainSyncLock(syncResponse) { @@ -678,18 +686,29 @@ export class Session { } } + _tryReplaceRoomBeingCreated(roomId) { + console.trace("_tryReplaceRoomBeingCreated " + roomId); + for (const [,roomBeingCreated] of this._roomsBeingCreated) { + if (roomBeingCreated.roomId === roomId) { + const observableStatus = this._observedRoomStatus.get(roomBeingCreated.localId); + if (observableStatus) { + console.log("marking room as replaced", observableStatus.get()); + observableStatus.set(observableStatus.get() | RoomStatus.Replaced); + } else { + console.log("no observableStatus"); + } + this._roomsBeingCreated.remove(roomBeingCreated.localId); + return; + } + } + } + applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates) { // update the collections after sync for (const rs of roomStates) { if (rs.shouldAdd) { this._rooms.add(rs.id, rs.room); - for (const roomBeingCreated of this._roomsBeingCreated) { - if (roomBeingCreated.roomId === rs.id) { - roomBeingCreated.notifyJoinedRoom(); - this._roomsBeingCreated.delete(roomBeingCreated.localId); - break; - } - } + this._tryReplaceRoomBeingCreated(rs.id); } else if (rs.shouldRemove) { this._rooms.remove(rs.id); } @@ -707,12 +726,12 @@ export class Session { if (this._observedRoomStatus.size !== 0) { for (const ars of archivedRoomStates) { if (ars.shouldAdd) { - this._observedRoomStatus.get(ars.id)?.set(RoomStatus.archived); + this._observedRoomStatus.get(ars.id)?.set(RoomStatus.Archived); } } for (const rs of roomStates) { if (rs.shouldAdd) { - this._observedRoomStatus.get(rs.id)?.set(RoomStatus.joined); + this._observedRoomStatus.get(rs.id)?.set(RoomStatus.Joined); } } for (const is of inviteStates) { @@ -731,7 +750,7 @@ export class Session { _forgetArchivedRoom(roomId) { const statusObservable = this._observedRoomStatus.get(roomId); if (statusObservable) { - statusObservable.set(statusObservable.get().withoutArchived()); + statusObservable.set(statusObservable.get() ^ RoomStatus.Archived); } } @@ -820,21 +839,25 @@ export class Session { } async getRoomStatus(roomId) { + const isBeingCreated = !!this._roomsBeingCreated.get(roomId); + if (isBeingCreated) { + return RoomStatus.BeingCreated; + } const isJoined = !!this._rooms.get(roomId); if (isJoined) { - return RoomStatus.joined; + return RoomStatus.Joined; } else { const isInvited = !!this._invites.get(roomId); const txn = await this._storage.readTxn([this._storage.storeNames.archivedRoomSummary]); const isArchived = await txn.archivedRoomSummary.has(roomId); if (isInvited && isArchived) { - return RoomStatus.invitedAndArchived; + return RoomStatus.Invited | RoomStatus.Archived; } else if (isInvited) { - return RoomStatus.invited; + return RoomStatus.Invited; } else if (isArchived) { - return RoomStatus.archived; + return RoomStatus.Archived; } else { - return RoomStatus.none; + return RoomStatus.None; } } } @@ -843,9 +866,10 @@ export class Session { let observable = this._observedRoomStatus.get(roomId); if (!observable) { const status = await this.getRoomStatus(roomId); - observable = new RetainedObservableValue(status, () => { + observable = new FooRetainedObservableValue(status, () => { this._observedRoomStatus.delete(roomId); }); + this._observedRoomStatus.set(roomId, observable); } return observable; @@ -897,6 +921,13 @@ export class Session { } } +class FooRetainedObservableValue extends RetainedObservableValue { + set(value) { + console.log("setting room status to", value); + super.set(value); + } +} + export function tests() { function createStorageMock(session, pendingEvents = []) { return { diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index aaf66be1..46c06af0 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -231,6 +231,7 @@ export class Room extends BaseRoom { } } let emitChange = false; + console.log("Room summaryChanges", this.id, summaryChanges); if (summaryChanges) { this._summary.applyChanges(summaryChanges); if (!this._summary.data.needsHeroes) { diff --git a/src/matrix/room/RoomStatus.js b/src/matrix/room/RoomStatus.js deleted file mode 100644 index 884103e2..00000000 --- a/src/matrix/room/RoomStatus.js +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -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. -*/ - -export class RoomStatus { - constructor(joined, invited, archived) { - this.joined = joined; - this.invited = invited; - this.archived = archived; - } - - withInvited() { - if (this.invited) { - return this; - } else if (this.archived) { - return RoomStatus.invitedAndArchived; - } else { - return RoomStatus.invited; - } - } - - withoutInvited() { - if (!this.invited) { - return this; - } else if (this.joined) { - return RoomStatus.joined; - } else if (this.archived) { - return RoomStatus.archived; - } else { - return RoomStatus.none; - } - } - - withoutArchived() { - if (!this.archived) { - return this; - } else if (this.invited) { - return RoomStatus.invited; - } else { - return RoomStatus.none; - } - } -} - -RoomStatus.joined = new RoomStatus(true, false, false); -RoomStatus.archived = new RoomStatus(false, false, true); -RoomStatus.invited = new RoomStatus(false, true, false); -RoomStatus.invitedAndArchived = new RoomStatus(false, true, true); -RoomStatus.none = new RoomStatus(false, false, false); diff --git a/src/matrix/room/RoomStatus.ts b/src/matrix/room/RoomStatus.ts new file mode 100644 index 00000000..f66f59d7 --- /dev/null +++ b/src/matrix/room/RoomStatus.ts @@ -0,0 +1,24 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +export enum RoomStatus { + None = 1 << 0, + BeingCreated = 1 << 1, + Invited = 1 << 2, + Joined = 1 << 3, + Replaced = 1 << 4, + Archived = 1 << 5, +} diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index 934650c0..24adfcf1 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -16,6 +16,7 @@ limitations under the License. import {calculateRoomName} from "./members/Heroes"; import {createRoomEncryptionEvent} from "../e2ee/common"; +import {MediaRepository} from "../net/MediaRepository"; import {EventEmitter} from "../../utils/EventEmitter"; import type {StateEvent} from "../storage/types"; @@ -58,32 +59,35 @@ function presetForType(type: RoomType): string { } } -export class RoomBeingCreated extends EventEmitter<{change: never, joined: string}> { +export class RoomBeingCreated extends EventEmitter<{change: never}> { private _roomId?: string; private profiles: Profile[] = []; public readonly isEncrypted: boolean; - public readonly name: string; + private _name: string; constructor( - private readonly localId: string, + public readonly localId: string, private readonly type: RoomType, isEncrypted: boolean | undefined, private readonly explicitName: string | undefined, private readonly topic: string | undefined, private readonly inviteUserIds: string[] | undefined, + private readonly updateCallback, + public readonly mediaRepository: MediaRepository, log: ILogItem ) { super(); this.isEncrypted = isEncrypted === undefined ? defaultE2EEStatusForType(this.type) : isEncrypted; if (explicitName) { - this.name = explicitName; + this._name = explicitName; } else { const summaryData = { joinCount: 1, // ourselves inviteCount: (this.inviteUserIds?.length || 0) }; - this.name = calculateRoomName(this.profiles, summaryData, log); + const userIdProfiles = (inviteUserIds || []).map(userId => new UserIdProfile(userId)); + this._name = calculateRoomName(userIdProfiles, summaryData, log); } } @@ -111,31 +115,52 @@ export class RoomBeingCreated extends EventEmitter<{change: never, joined: strin if (this.isEncrypted) { options.initial_state = [createRoomEncryptionEvent()]; } - + console.log("going to create the room now"); const response = await hsApi.createRoom(options, {log}).response(); this._roomId = response["room_id"]; - this.emit("change"); + console.log("done creating the room now", this._roomId); + // TODO: somehow check in Session if we need to replace this with a joined room + // in case the room appears first in sync, and this request returns later + this.emitChange(); } private async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise { // only load profiles if we need it for the room name and avatar if (!this.explicitName && this.inviteUserIds) { this.profiles = await loadProfiles(this.inviteUserIds, hsApi, log); - this.emit("change"); + console.log("loaded the profiles", this.profiles); + const summaryData = { + joinCount: 1, // ourselves + inviteCount: this.inviteUserIds.length + }; + this._name = calculateRoomName(this.profiles, summaryData, log); + console.log("loaded the profiles and the new name", this.name); + this.emitChange(); } } - notifyJoinedRoom() { - this.emit("joined", this._roomId); + private emitChange() { + this.updateCallback(this); + this.emit("change"); + } + + get avatarColorId(): string { + return this.inviteUserIds?.[0] ?? this._roomId ?? this.localId; } get avatarUrl(): string | undefined { - return this.profiles[0]?.avatarUrl; + const result = this.profiles[0]?.avatarUrl; + console.log("RoomBeingCreated.avatarUrl", this.profiles, result); + return result; } get roomId(): string | undefined { return this._roomId; } + + get name() { return this._name; } + + get isBeingCreated(): boolean { return true; } } export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { @@ -149,8 +174,8 @@ export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: interface IProfile { get userId(): string; - get displayName(): string; - get avatarUrl(): string; + get displayName(): string | undefined; + get avatarUrl(): string | undefined; get name(): string; } @@ -158,8 +183,16 @@ export class Profile implements IProfile { constructor( public readonly userId: string, public readonly displayName: string, - public readonly avatarUrl: string + public readonly avatarUrl: string | undefined ) {} get name() { return this.displayName || this.userId; } } + +class UserIdProfile implements IProfile { + constructor(public readonly userId: string) {} + get displayName() { return undefined; } + get name() { return this.userId; } + get avatarUrl() { return undefined; } + +} diff --git a/src/platform/web/ui/session/RoomGridView.js b/src/platform/web/ui/session/RoomGridView.js index fc6497da..79fc3d21 100644 --- a/src/platform/web/ui/session/RoomGridView.js +++ b/src/platform/web/ui/session/RoomGridView.js @@ -15,6 +15,7 @@ limitations under the License. */ import {RoomView} from "./room/RoomView.js"; +import {RoomBeingCreatedView} from "./room/RoomBeingCreatedView.js"; import {InviteView} from "./room/InviteView.js"; import {TemplateView} from "../general/TemplateView"; import {StaticView} from "../general/StaticView.js"; @@ -33,7 +34,9 @@ export class RoomGridView extends TemplateView { }, }, t.mapView(vm => vm.roomViewModelAt(i), roomVM => { if (roomVM) { - if (roomVM.kind === "invite") { + if (roomVM.kind === "roomBeingCreated") { + return new RoomBeingCreatedView(roomVM); + } else if (roomVM.kind === "invite") { return new InviteView(roomVM); } else { return new RoomView(roomVM); diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index f2ac2971..3740ffca 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -18,6 +18,7 @@ limitations under the License. import {LeftPanelView} from "./leftpanel/LeftPanelView.js"; import {RoomView} from "./room/RoomView.js"; import {UnknownRoomView} from "./room/UnknownRoomView.js"; +import {RoomBeingCreatedView} from "./room/RoomBeingCreatedView.js"; import {InviteView} from "./room/InviteView.js"; import {LightboxView} from "./room/LightboxView.js"; import {TemplateView} from "../general/TemplateView"; @@ -48,6 +49,8 @@ export class SessionView extends TemplateView { return new InviteView(vm.currentRoomViewModel); } else if (vm.currentRoomViewModel.kind === "room") { return new RoomView(vm.currentRoomViewModel); + } else if (vm.currentRoomViewModel.kind === "roomBeingCreated") { + return new RoomBeingCreatedView(vm.currentRoomViewModel); } else { return new UnknownRoomView(vm.currentRoomViewModel); } diff --git a/src/platform/web/ui/session/leftpanel/InviteTileView.js b/src/platform/web/ui/session/leftpanel/InviteTileView.js index b99ab1c6..de064bed 100644 --- a/src/platform/web/ui/session/leftpanel/InviteTileView.js +++ b/src/platform/web/ui/session/leftpanel/InviteTileView.js @@ -16,7 +16,7 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView"; -import {renderStaticAvatar} from "../../avatar.js"; +import {AvatarView} from "../../AvatarView"; import {spinner} from "../../common.js"; export class InviteTileView extends TemplateView { @@ -27,9 +27,9 @@ export class InviteTileView extends TemplateView { }; return t.li({"className": classes}, [ t.a({href: vm.url}, [ - renderStaticAvatar(vm, 32), + t.view(new AvatarView(vm, 32), {parentProvidesUpdates: true}), t.div({className: "description"}, [ - t.div({className: "name"}, vm.name), + t.div({className: "name"}, vm => vm.name), t.map(vm => vm.busy, busy => { if (busy) { return spinner(t); @@ -41,4 +41,10 @@ export class InviteTileView extends TemplateView { ]) ]); } + + update(value, props) { + super.update(value); + // update the AvatarView as we told it to not subscribe itself with parentProvidesUpdates + this.updateSubViews(value, props); + } } diff --git a/src/platform/web/ui/session/leftpanel/LeftPanelView.js b/src/platform/web/ui/session/leftpanel/LeftPanelView.js index 481c5362..bc14fa1e 100644 --- a/src/platform/web/ui/session/leftpanel/LeftPanelView.js +++ b/src/platform/web/ui/session/leftpanel/LeftPanelView.js @@ -64,7 +64,7 @@ export class LeftPanelView extends TemplateView { list: vm.tileViewModels, }, tileVM => { - if (tileVM.kind === "invite") { + if (tileVM.kind === "invite" || tileVM.kind === "roomBeingCreated") { return new InviteTileView(tileVM); } else { return new RoomTileView(tileVM); diff --git a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js index c249515a..5d2f9387 100644 --- a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js +++ b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js @@ -46,7 +46,8 @@ export class MemberDetailsView extends TemplateView { t.div({className: "MemberDetailsView_label"}, vm.i18n`Options`), t.div({className: "MemberDetailsView_options"}, [ - t.a({href: vm.linkToUser, target: "_blank", rel: "noopener"}, vm.i18n`Open Link to User`) + t.a({href: vm.linkToUser, target: "_blank", rel: "noopener"}, vm.i18n`Open Link to User`), + t.button({className: "text", onClick: () => vm.openDirectMessage()}, vm.i18n`Open direct message`) ]) ]); } diff --git a/src/platform/web/ui/session/room/MessageComposer.js b/src/platform/web/ui/session/room/MessageComposer.js index f92c4403..9c67fa9f 100644 --- a/src/platform/web/ui/session/room/MessageComposer.js +++ b/src/platform/web/ui/session/room/MessageComposer.js @@ -39,7 +39,7 @@ export class MessageComposer extends TemplateView { this._clearHeight(); } }, - placeholder: vm.isEncrypted ? "Send an encrypted message…" : "Send a message…", + placeholder: vm => vm.isEncrypted ? "Send an encrypted message…" : "Send a message…", rows: "1" }); this._focusInput = () => this._input.focus(); diff --git a/src/platform/web/ui/session/room/RoomBeingCreatedView.js b/src/platform/web/ui/session/room/RoomBeingCreatedView.js new file mode 100644 index 00000000..df26a43d --- /dev/null +++ b/src/platform/web/ui/session/room/RoomBeingCreatedView.js @@ -0,0 +1,25 @@ +/* +Copyright 2020 Bruno Windels +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +import {TemplateView} from "../../general/TemplateView"; +import {renderStaticAvatar} from "../../avatar.js"; + +export class RoomBeingCreatedView extends TemplateView { + render(t, vm) { + return t.h1({className: "middle"}, ["creating room", vm => vm.name]); + } +} From 3d8b9cce41b347eb68b2d363a991874ff4677473 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 4 Feb 2022 15:41:37 +0530 Subject: [PATCH 102/160] Fix responseCode in Request --- src/matrix/net/RequestScheduler.ts | 45 +++++++++++++++++++----------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/matrix/net/RequestScheduler.ts b/src/matrix/net/RequestScheduler.ts index e36a3541..e2118c5e 100644 --- a/src/matrix/net/RequestScheduler.ts +++ b/src/matrix/net/RequestScheduler.ts @@ -25,25 +25,33 @@ import type {IHomeServerRequest} from "./HomeServerRequest.js"; class Request implements IHomeServerRequest { public readonly methodName: string; public readonly args: any[]; - public resolve: (result: any) => void; - public reject: (error: Error) => void; - public requestResult?: IHomeServerRequest; + private responseResolve: (result: any) => void; + public responseReject: (error: Error) => void; + private responseCodeResolve: (result: any) => void; + private responseCodeReject: (result: any) => void; + private _requestResult?: IHomeServerRequest; private readonly _responsePromise: Promise; + private readonly _responseCodePromise: Promise; constructor(methodName: string, args: any[]) { this.methodName = methodName; this.args = args; this._responsePromise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; + this.responseResolve = resolve; + this.responseReject = reject; + }); + this._responseCodePromise = new Promise((resolve, reject) => { + this.responseCodeResolve = resolve; + this.responseCodeReject = reject; }); } abort(): void { - if (this.requestResult) { - this.requestResult.abort(); + if (this._requestResult) { + this._requestResult.abort(); } else { - this.reject(new AbortError()); + this.responseReject(new AbortError()); + this.responseCodeReject(new AbortError()); } } @@ -51,11 +59,18 @@ class Request implements IHomeServerRequest { return this._responsePromise; } - async responseCode(): Promise { - let resolve; - const promise: Promise = new Promise(r => resolve = r); - this.requestResult?.responseCode().then(code => resolve(code)); - return promise; + responseCode(): Promise { + return this._responseCodePromise; + } + + set requestResult(result) { + this._requestResult = result; + this._requestResult?.response().then(response => this.responseResolve(response)); + this._requestResult?.responseCode().then(response => this.responseCodeResolve(response)); + } + + get requestResult() { + return this._requestResult; } } @@ -121,8 +136,6 @@ export class RequestScheduler { ].apply(this._hsApi, request.args); // so the request can be aborted request.requestResult = requestResult; - const response = await requestResult.response(); - request.resolve(response); return; } catch (err) { if ( @@ -142,7 +155,7 @@ export class RequestScheduler { await retryDelay.waitForRetry(); } } else { - request.reject(err); + request.responseReject(err); return; } } From 0b04612d6ce46c0e26512d642884161b4bd8c375 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Feb 2022 11:16:58 +0100 Subject: [PATCH 103/160] WIP2 --- src/domain/session/room/RoomViewModel.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index ace933fb..c81c47ef 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -97,9 +97,8 @@ export class RoomViewModel extends ViewModel { // room doesn't tell us yet which fields changed, // so emit all fields originating from summary _onRoomChange() { - if (this._room.isArchived) { - this._composerVM.emitChange(); - } + // propagate the update to the child view models so it's bindings can update based on room changes + this._composerVM.emitChange(); this.emitChange(); } From 32af7e6f0980b8c9c853dce8e5a665cdade243e4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 4 Feb 2022 16:23:39 +0530 Subject: [PATCH 104/160] Make more changes - make setter a method - lazily create promise --- src/matrix/net/RequestScheduler.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/matrix/net/RequestScheduler.ts b/src/matrix/net/RequestScheduler.ts index e2118c5e..cee4142f 100644 --- a/src/matrix/net/RequestScheduler.ts +++ b/src/matrix/net/RequestScheduler.ts @@ -31,7 +31,7 @@ class Request implements IHomeServerRequest { private responseCodeReject: (result: any) => void; private _requestResult?: IHomeServerRequest; private readonly _responsePromise: Promise; - private readonly _responseCodePromise: Promise; + private _responseCodePromise: Promise; constructor(methodName: string, args: any[]) { this.methodName = methodName; @@ -40,10 +40,6 @@ class Request implements IHomeServerRequest { this.responseResolve = resolve; this.responseReject = reject; }); - this._responseCodePromise = new Promise((resolve, reject) => { - this.responseCodeResolve = resolve; - this.responseCodeReject = reject; - }); } abort(): void { @@ -60,13 +56,22 @@ class Request implements IHomeServerRequest { } responseCode(): Promise { + if (this.requestResult) { + return this.requestResult.responseCode(); + } + if (!this._responseCodePromise) { + this._responseCodePromise = new Promise((resolve, reject) => { + this.responseCodeResolve = resolve; + this.responseCodeReject = reject; + }); + } return this._responseCodePromise; } - set requestResult(result) { + setRequestResult(result) { this._requestResult = result; this._requestResult?.response().then(response => this.responseResolve(response)); - this._requestResult?.responseCode().then(response => this.responseCodeResolve(response)); + this._requestResult?.responseCode().then(response => this.responseCodeResolve?.(response)); } get requestResult() { @@ -135,7 +140,7 @@ export class RequestScheduler { request.methodName ].apply(this._hsApi, request.args); // so the request can be aborted - request.requestResult = requestResult; + request.setRequestResult(requestResult); return; } catch (err) { if ( From 891375a885b513d90054a02cab066f10ba62b4aa Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 4 Feb 2022 16:35:47 +0530 Subject: [PATCH 105/160] Rename allowerErrors -> allowedStatusCodes --- src/matrix/net/HomeServerApi.ts | 4 ++-- src/matrix/net/HomeServerRequest.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index 88ff32d9..605607b6 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -38,7 +38,7 @@ type Options = { type BaseRequestOptions = { log?: ILogItem; - allowedErrors?: number[]; + allowedStatusCodes?: number[]; uploadProgress?: (loadedBytes: number) => void; timeout?: number; prefix?: string; @@ -165,7 +165,7 @@ export class HomeServerApi { } register(username: string | null, password: string, initialDeviceDisplayName: string, auth?: Record, inhibitLogin: boolean = true , options: BaseRequestOptions = {}): IHomeServerRequest { - options.allowedErrors = [401]; + options.allowedStatusCodes = [401]; const body: any = { auth, password, diff --git a/src/matrix/net/HomeServerRequest.ts b/src/matrix/net/HomeServerRequest.ts index ee4ad2ba..d6745d03 100644 --- a/src/matrix/net/HomeServerRequest.ts +++ b/src/matrix/net/HomeServerRequest.ts @@ -27,7 +27,7 @@ export interface IHomeServerRequest { type HomeServerRequestOptions = { log?: ILogItem; - allowedErrors?: number[]; + allowedStatusCodes?: number[]; }; export class HomeServerRequest implements IHomeServerRequest { @@ -47,7 +47,7 @@ export class HomeServerRequest implements IHomeServerRequest { this._promise = sourceRequest.response().then(response => { log?.set("status", response.status); // ok? - if (response.status >= 200 && response.status < 300 || options?.allowedErrors?.includes(response.status)) { + if (response.status >= 200 && response.status < 300 || options?.allowedStatusCodes?.includes(response.status)) { log?.finish(); return response.body; } else { From e8c480426ae9bf92ac7a946e48a1206c7e457640 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 4 Feb 2022 16:37:43 +0530 Subject: [PATCH 106/160] Remove error code --- src/matrix/registration/Registration.ts | 4 ---- src/matrix/registration/types.ts | 8 +------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index d50cbca5..c3b91e3b 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -101,10 +101,6 @@ export class Registration { else { throw new Error("This stage could not be completed!"); } - case 400: - default: - const error = "error" in response? response.error: "Could not parse response"; - throw new Error(error); } } diff --git a/src/matrix/registration/types.ts b/src/matrix/registration/types.ts index 77d051af..f1ddbe98 100644 --- a/src/matrix/registration/types.ts +++ b/src/matrix/registration/types.ts @@ -21,7 +21,7 @@ export type AccountDetails = { inhibitLogin: boolean; } -export type RegistrationResponse = RegistrationResponseMoreDataNeeded | RegistrationResponseError | RegistrationResponseSuccess; +export type RegistrationResponse = RegistrationResponseMoreDataNeeded | RegistrationResponseSuccess; export type RegistrationResponseMoreDataNeeded = { completed?: string[]; @@ -31,12 +31,6 @@ export type RegistrationResponseMoreDataNeeded = { status: 401; } -type RegistrationResponseError = { - errcode: string; - error: string; - status: 400; -} - export type RegistrationResponseSuccess = { user_id: string; device_id: string; From e66549a0671fcec9a9c7b382df5df2af2c99ef48 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 4 Feb 2022 16:40:49 +0530 Subject: [PATCH 107/160] Remove dead code --- src/matrix/registration/Registration.ts | 2 +- src/matrix/registration/registrationStageFromType.ts | 5 ++--- src/matrix/registration/stages/BaseRegistrationStage.ts | 6 +----- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index c3b91e3b..ad0b461e 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -77,7 +77,7 @@ export class Registration { if (!stageClass) { throw new Error(`Unknown stage: ${stage}`); } - const registrationStage = new stageClass(this._hsApi, this._accountDetails, session, params?.[stage]); + const registrationStage = new stageClass(session, params?.[stage]); if (!firstStage) { firstStage = registrationStage; lastStage = registrationStage; diff --git a/src/matrix/registration/registrationStageFromType.ts b/src/matrix/registration/registrationStageFromType.ts index 6b120a6f..cf08e0ec 100644 --- a/src/matrix/registration/registrationStageFromType.ts +++ b/src/matrix/registration/registrationStageFromType.ts @@ -15,12 +15,11 @@ limitations under the License. */ import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; -import type {HomeServerApi} from "../net/HomeServerApi"; -import type {AccountDetails, RegistrationParams} from "./types"; +import type {RegistrationParams} from "./types"; import {DummyAuth} from "./stages/DummyAuth"; import {TermsAuth} from "./stages/TermsAuth"; -type ClassDerivedFromBaseRegistration = { new(hsApi: HomeServerApi, registrationData: AccountDetails, session: string, params?: RegistrationParams): BaseRegistrationStage } & typeof BaseRegistrationStage; +type ClassDerivedFromBaseRegistration = { new(session: string, params?: RegistrationParams): BaseRegistrationStage } & typeof BaseRegistrationStage; export function registrationStageFromType(type: string): ClassDerivedFromBaseRegistration | undefined{ switch (type) { diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 964cc75d..426dbf2c 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -18,15 +18,11 @@ import type {HomeServerApi} from "../../net/HomeServerApi"; import type {AccountDetails, AuthenticationData, RegistrationParams} from "../types"; export abstract class BaseRegistrationStage { - protected readonly _hsApi: HomeServerApi; - protected readonly _accountDetails: AccountDetails; protected _session: string; protected _nextStage: BaseRegistrationStage; protected readonly _params?: Record - constructor(hsApi: HomeServerApi, accountDetails: AccountDetails, session: string, params?: RegistrationParams) { - this._hsApi = hsApi; - this._accountDetails = accountDetails; + constructor(session: string, params?: RegistrationParams) { this._session = session; this._params = params; } From 22d5505a2b7932d8a8257d9a5e3c3f1f6419b3da Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 4 Feb 2022 16:50:22 +0530 Subject: [PATCH 108/160] Create registration stage in Registration itself --- src/matrix/registration/Registration.ts | 21 +++++++++---- .../registration/registrationStageFromType.ts | 31 ------------------- 2 files changed, 15 insertions(+), 37 deletions(-) delete mode 100644 src/matrix/registration/registrationStageFromType.ts diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index ad0b461e..3d8ec9fc 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -15,14 +15,16 @@ limitations under the License. */ import type {HomeServerApi} from "../net/HomeServerApi"; -import {registrationStageFromType} from "./registrationStageFromType"; import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; +import {DummyAuth} from "./stages/DummyAuth"; +import {TermsAuth} from "./stages/TermsAuth"; import type { AccountDetails, RegistrationFlow, RegistrationResponseMoreDataNeeded, RegistrationResponse, RegistrationResponseSuccess, + RegistrationParams, } from "./types"; type FlowSelector = (flows: RegistrationFlow[]) => RegistrationFlow | void; @@ -73,11 +75,7 @@ export class Registration { let firstStage: BaseRegistrationStage | undefined; let lastStage: BaseRegistrationStage; for (const stage of flow.stages) { - const stageClass = registrationStageFromType(stage); - if (!stageClass) { - throw new Error(`Unknown stage: ${stage}`); - } - const registrationStage = new stageClass(session, params?.[stage]); + const registrationStage = this._createRegistrationStage(stage, session, params); if (!firstStage) { firstStage = registrationStage; lastStage = registrationStage; @@ -104,6 +102,17 @@ export class Registration { } } + private _createRegistrationStage(type: string, session: string, params?: RegistrationParams) { + switch (type) { + case "m.login.dummy": + return new DummyAuth(session, params); + case "m.login.terms": + return new TermsAuth(session, params); + default: + throw new Error(`Unknown stage: ${type}`); + } + } + get sessionInfo(): RegistrationResponseSuccess | undefined { return this._sessionInfo; } diff --git a/src/matrix/registration/registrationStageFromType.ts b/src/matrix/registration/registrationStageFromType.ts deleted file mode 100644 index cf08e0ec..00000000 --- a/src/matrix/registration/registrationStageFromType.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -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. -*/ - -import type {BaseRegistrationStage} from "./stages/BaseRegistrationStage"; -import type {RegistrationParams} from "./types"; -import {DummyAuth} from "./stages/DummyAuth"; -import {TermsAuth} from "./stages/TermsAuth"; - -type ClassDerivedFromBaseRegistration = { new(session: string, params?: RegistrationParams): BaseRegistrationStage } & typeof BaseRegistrationStage; - -export function registrationStageFromType(type: string): ClassDerivedFromBaseRegistration | undefined{ - switch (type) { - case "m.login.dummy": - return DummyAuth; - case "m.login.terms": - return TermsAuth; - } -} From 028b96e4c547302389a09efa63cd8e65a22dde68 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 4 Feb 2022 17:11:33 +0530 Subject: [PATCH 109/160] Let type also be undefined --- src/matrix/registration/Registration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index 3d8ec9fc..a0b4eb89 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -73,7 +73,7 @@ export class Registration { throw new Error("flowSelector did not return any flow!"); } let firstStage: BaseRegistrationStage | undefined; - let lastStage: BaseRegistrationStage; + let lastStage: BaseRegistrationStage | undefined; for (const stage of flow.stages) { const registrationStage = this._createRegistrationStage(stage, session, params); if (!firstStage) { From 2ac63e78cade3c7dd635f0cdf28349db39ed069d Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Fri, 4 Feb 2022 17:16:15 +0530 Subject: [PATCH 110/160] mark method as internal Co-authored-by: Bruno Windels --- src/matrix/registration/stages/BaseRegistrationStage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 964cc75d..c9350fdb 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -40,6 +40,7 @@ export abstract class BaseRegistrationStage { * This method should return auth part that must be provided to * /register endpoint to successfully complete this stage */ + /** @internal */ abstract generateAuthenticationData(): AuthenticationData; setNextStage(stage: BaseRegistrationStage) { From 0828ac12b1d2e0bbbe8445f18b12927b9c3f067e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 4 Feb 2022 17:25:15 +0530 Subject: [PATCH 111/160] Fix params --- src/matrix/registration/Registration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/registration/Registration.ts b/src/matrix/registration/Registration.ts index a0b4eb89..c9c9af87 100644 --- a/src/matrix/registration/Registration.ts +++ b/src/matrix/registration/Registration.ts @@ -105,9 +105,9 @@ export class Registration { private _createRegistrationStage(type: string, session: string, params?: RegistrationParams) { switch (type) { case "m.login.dummy": - return new DummyAuth(session, params); + return new DummyAuth(session, params?.[type]); case "m.login.terms": - return new TermsAuth(session, params); + return new TermsAuth(session, params?.[type]); default: throw new Error(`Unknown stage: ${type}`); } From a163cee18d0d21eed3134f8c00fa9a0065e45d12 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 4 Feb 2022 17:25:30 +0530 Subject: [PATCH 112/160] Remove dead imports --- src/matrix/registration/stages/BaseRegistrationStage.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/matrix/registration/stages/BaseRegistrationStage.ts b/src/matrix/registration/stages/BaseRegistrationStage.ts index 71972a64..cc5f46c1 100644 --- a/src/matrix/registration/stages/BaseRegistrationStage.ts +++ b/src/matrix/registration/stages/BaseRegistrationStage.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type {HomeServerApi} from "../../net/HomeServerApi"; -import type {AccountDetails, AuthenticationData, RegistrationParams} from "../types"; +import type {AuthenticationData, RegistrationParams} from "../types"; export abstract class BaseRegistrationStage { protected _session: string; From f7f32ac80677c5e0388ca33b84d0d35e9cf68c62 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 4 Feb 2022 17:39:52 +0530 Subject: [PATCH 113/160] responseCodeReject may not exist --- src/matrix/net/RequestScheduler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/net/RequestScheduler.ts b/src/matrix/net/RequestScheduler.ts index cee4142f..276de639 100644 --- a/src/matrix/net/RequestScheduler.ts +++ b/src/matrix/net/RequestScheduler.ts @@ -47,7 +47,7 @@ class Request implements IHomeServerRequest { this._requestResult.abort(); } else { this.responseReject(new AbortError()); - this.responseCodeReject(new AbortError()); + this.responseCodeReject?.(new AbortError()); } } From 28931f41035ff6db2861b53f083bc2970b89d8a1 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 4 Feb 2022 17:48:42 +0530 Subject: [PATCH 114/160] Use async/await --- src/matrix/net/RequestScheduler.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/matrix/net/RequestScheduler.ts b/src/matrix/net/RequestScheduler.ts index 276de639..dc5c501b 100644 --- a/src/matrix/net/RequestScheduler.ts +++ b/src/matrix/net/RequestScheduler.ts @@ -68,10 +68,12 @@ class Request implements IHomeServerRequest { return this._responseCodePromise; } - setRequestResult(result) { + async setRequestResult(result) { this._requestResult = result; - this._requestResult?.response().then(response => this.responseResolve(response)); - this._requestResult?.responseCode().then(response => this.responseCodeResolve?.(response)); + const response = await this._requestResult?.response(); + this.responseResolve(response); + const responseCode = await this._requestResult?.responseCode(); + this.responseCodeResolve(responseCode); } get requestResult() { @@ -140,7 +142,7 @@ export class RequestScheduler { request.methodName ].apply(this._hsApi, request.args); // so the request can be aborted - request.setRequestResult(requestResult); + await request.setRequestResult(requestResult); return; } catch (err) { if ( From 4a0db9f984b2d277fec08c9b6cc9d67a7b137e3b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 4 Feb 2022 18:28:17 +0530 Subject: [PATCH 115/160] Add required exports --- src/lib.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/lib.ts b/src/lib.ts index 0aa1bb44..3e191d45 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -26,3 +26,10 @@ export {RoomViewModel} from "./domain/session/room/RoomViewModel.js"; export {RoomView} from "./platform/web/ui/session/room/RoomView.js"; export {TimelineViewModel} from "./domain/session/room/timeline/TimelineViewModel.js"; export {TimelineView} from "./platform/web/ui/session/room/TimelineView"; +export {Navigation} from "./domain/navigation/Navigation.js"; +export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js"; +export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js"; +export {TemplateView} from "./platform/web/ui/general/TemplateView"; +export {ViewModel} from "./domain/ViewModel.js"; +export {LoadingView} from "./platform/web/ui/general/LoadingView.js"; +export {AvatarView} from "./platform/web/ui/AvatarView.js"; From 0bb3cfcfadec1e0b223133ce24ad7152fc83b30c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Feb 2022 17:49:10 +0100 Subject: [PATCH 116/160] WIP3 --- .../session/leftpanel/LeftPanelViewModel.js | 7 +- src/logging/LogItem.ts | 3 +- src/logging/types.ts | 2 +- src/matrix/Session.js | 18 ++--- src/matrix/Sync.js | 2 +- src/matrix/room/RoomSummary.js | 5 +- src/matrix/room/create.ts | 6 +- src/observable/map/LogMap.js | 70 +++++++++++++++++++ 8 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 src/observable/map/LogMap.js diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index d85e5943..597c0da6 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -21,6 +21,7 @@ import {InviteTileViewModel} from "./InviteTileViewModel.js"; import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js"; import {RoomFilter} from "./RoomFilter.js"; import {ApplyMap} from "../../../observable/map/ApplyMap.js"; +import {LogMap} from "../../../observable/map/LogMap.js"; import {addPanelIfNeeded} from "../../navigation/index.js"; export class LeftPanelViewModel extends ViewModel { @@ -38,7 +39,7 @@ export class LeftPanelViewModel extends ViewModel { _mapTileViewModels(roomsBeingCreated, invites, rooms) { // join is not commutative, invites will take precedence over rooms - return roomsBeingCreated.join(invites).join(rooms).mapValues((item, emitChange) => { + const joined = invites.join(roomsBeingCreated, rooms).mapValues((item, emitChange) => { let vm; if (item.isBeingCreated) { vm = new RoomBeingCreatedTileViewModel(this.childOptions({roomBeingCreated: item, emitChange})); @@ -54,6 +55,10 @@ export class LeftPanelViewModel extends ViewModel { } return vm; }); + return joined; + // return new LogMap(joined, (op, key, value) => { + // console.log("room list", op, key, value); + // }); } _updateCurrentVM(vm) { diff --git a/src/logging/LogItem.ts b/src/logging/LogItem.ts index cf3da284..b47b69c1 100644 --- a/src/logging/LogItem.ts +++ b/src/logging/LogItem.ts @@ -108,13 +108,14 @@ export class LogItem implements ILogItem { return item; } - set(key: string | object, value?: unknown): void { + set(key: string | object, value?: unknown): ILogItem { if(typeof key === "object") { const values = key; Object.assign(this._values, values); } else { this._values[key] = value; } + return this; } serialize(filter: LogFilter, parentStartTime: number | undefined, forced: boolean): ISerializedItem | undefined { diff --git a/src/logging/types.ts b/src/logging/types.ts index f4a20ee0..bf9861a5 100644 --- a/src/logging/types.ts +++ b/src/logging/types.ts @@ -43,7 +43,7 @@ export interface ILogItem { readonly values: LogItemValues; wrap(labelOrValues: LabelOrValues, callback: LogCallback, logLevel?: LogLevel, filterCreator?: FilterCreator): T; log(labelOrValues: LabelOrValues, logLevel?: LogLevel): ILogItem; - set(key: string | object, value: unknown): void; + set(key: string | object, value: unknown): ILogItem; runDetached(labelOrValues: LabelOrValues, callback: LogCallback, logLevel?: LogLevel, filterCreator?: FilterCreator): ILogItem; wrapDetached(labelOrValues: LabelOrValues, callback: LogCallback, logLevel?: LogLevel, filterCreator?: FilterCreator): void; refDetached(logItem: ILogItem, logLevel?: LogLevel): void; diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 49ae0f9c..4a9b0bad 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -64,10 +64,10 @@ export class Session { this._activeArchivedRooms = new Map(); this._invites = new ObservableMap(); this._inviteUpdateCallback = (invite, params) => this._invites.update(invite.id, params); - this._roomsBeingCreatedUpdateCallback = (rbc, params) => { + this._roomsBeingCreatedUpdateCallback = (rbc, params, log) => { this._roomsBeingCreated.update(rbc.localId, params); if (rbc.roomId && !!this.rooms.get(rbc.roomId)) { - this._tryReplaceRoomBeingCreated(rbc.roomId); + this._tryReplaceRoomBeingCreated(rbc.roomId, log); } }; this._roomsBeingCreated = new ObservableMap(); @@ -686,16 +686,16 @@ export class Session { } } - _tryReplaceRoomBeingCreated(roomId) { - console.trace("_tryReplaceRoomBeingCreated " + roomId); + _tryReplaceRoomBeingCreated(roomId, log) { for (const [,roomBeingCreated] of this._roomsBeingCreated) { if (roomBeingCreated.roomId === roomId) { const observableStatus = this._observedRoomStatus.get(roomBeingCreated.localId); if (observableStatus) { - console.log("marking room as replaced", observableStatus.get()); + this._platform.logger.wrapOrRun(log, `replacing room being created`, log => { + log.set("localId", roomBeingCreated.localId) + .set("roomId", roomBeingCreated.roomId); + }); observableStatus.set(observableStatus.get() | RoomStatus.Replaced); - } else { - console.log("no observableStatus"); } this._roomsBeingCreated.remove(roomBeingCreated.localId); return; @@ -703,12 +703,12 @@ export class Session { } } - applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates) { + applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates, log) { // update the collections after sync for (const rs of roomStates) { if (rs.shouldAdd) { this._rooms.add(rs.id, rs.room); - this._tryReplaceRoomBeingCreated(rs.id); + this._tryReplaceRoomBeingCreated(rs.id, log); } else if (rs.shouldRemove) { this._rooms.remove(rs.id); } diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index e9faa89b..5eb50b5c 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -316,7 +316,7 @@ export class Sync { for(let is of inviteStates) { log.wrap("invite", log => is.invite.afterSync(is.changes, log), log.level.Detail); } - this._session.applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates); + this._session.applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates, log); } _openSyncTxn() { diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index d0c78659..82087200 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -96,7 +96,10 @@ function processRoomAccountData(data, event) { } export function processStateEvent(data, event) { - if (event.type === "m.room.encryption") { + if (event.type === "m.room.create") { + data = data.cloneIfNeeded(); + data.lastMessageTimestamp = event.origin_server_ts; + } else if (event.type === "m.room.encryption") { const algorithm = event.content?.algorithm; if (!data.encryption && algorithm === MEGOLM_ALGORITHM) { data = data.cloneIfNeeded(); diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index 24adfcf1..c42da436 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -121,7 +121,7 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { console.log("done creating the room now", this._roomId); // TODO: somehow check in Session if we need to replace this with a joined room // in case the room appears first in sync, and this request returns later - this.emitChange(); + this.emitChange(undefined, log); } private async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise { @@ -139,8 +139,8 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { } } - private emitChange() { - this.updateCallback(this); + private emitChange(params?, log?) { + this.updateCallback(this, params, log); this.emit("change"); } diff --git a/src/observable/map/LogMap.js b/src/observable/map/LogMap.js new file mode 100644 index 00000000..4b8bb686 --- /dev/null +++ b/src/observable/map/LogMap.js @@ -0,0 +1,70 @@ +/* +Copyright 2020 Bruno Windels + +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. +*/ + +import {BaseObservableMap} from "./BaseObservableMap.js"; + +export class LogMap extends BaseObservableMap { + constructor(source, log) { + super(); + this._source = source; + this.log = log; + this._subscription = null; + } + + onAdd(key, value) { + this.log("add", key, value); + this.emitAdd(key, value); + } + + onRemove(key, value) { + this.log("remove", key, value); + this.emitRemove(key, value); + } + + onUpdate(key, value, params) { + this.log("update", key, value, params); + this.emitUpdate(key, value, params); + } + + onSubscribeFirst() { + this.log("subscribeFirst"); + this._subscription = this._source.subscribe(this); + super.onSubscribeFirst(); + } + + onUnsubscribeLast() { + super.onUnsubscribeLast(); + this._subscription = this._subscription(); + this.log("unsubscribeLast"); + } + + onReset() { + this.log("reset"); + this.emitReset(); + } + + [Symbol.iterator]() { + return this._source[Symbol.iterator](); + } + + get size() { + return this._source.size; + } + + get(key) { + return this._source.get(key); + } +} From b8687343784070c53ca383f40ba1e10485121614 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 7 Feb 2022 11:05:28 +0100 Subject: [PATCH 117/160] change sdk version --- scripts/sdk/base-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sdk/base-manifest.json b/scripts/sdk/base-manifest.json index d3e21d7b..3ee2ca3b 100644 --- a/scripts/sdk/base-manifest.json +++ b/scripts/sdk/base-manifest.json @@ -1,7 +1,7 @@ { "name": "hydrogen-view-sdk", "description": "Embeddable matrix client library, including view components", - "version": "0.0.4", + "version": "0.0.5", "main": "./hydrogen.es.js", "type": "module" } From e1fbd1242e24c00fca042cb30715620659adfcf1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 7 Feb 2022 16:30:44 +0100 Subject: [PATCH 118/160] WIP 4 --- .../rightpanel/MemberDetailsViewModel.js | 4 +- src/matrix/Session.js | 23 ++++++----- src/matrix/room/create.ts | 41 ++++++++----------- 3 files changed, 30 insertions(+), 38 deletions(-) diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index 6effb90c..b0573bfa 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -83,7 +83,7 @@ export class MemberDetailsViewModel extends ViewModel { } async openDirectMessage() { - const localId = await this._session.createRoom(RoomType.DirectMessage, undefined, undefined, undefined, [this.userId]); - this.navigation.push("room", localId); + const roomBeingCreated = await this._session.createRoom(RoomType.DirectMessage, undefined, undefined, undefined, [this.userId]); + this.navigation.push("room", roomBeingCreated.localId); } } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 4a9b0bad..02635855 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -594,13 +594,13 @@ export class Session { createRoom(type, isEncrypted, explicitName, topic, invites, log = undefined) { return this._platform.logger.wrapOrRun(log, "create room", log => { - const localId = `room-being-created-${this._platform.random()}`; + const localId = `local-${Math.round(this._platform.random() * Math.MAX_SAFE_INTEGER)}`; const roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, explicitName, topic, invites, this._roomsBeingCreatedUpdateCallback, this._mediaRepository, log); this._roomsBeingCreated.set(localId, roomBeingCreated); - log.wrapDetached("create room network", async log => { - roomBeingCreated.start(this._hsApi, log); + log.wrapDetached("create room network", log => { + return roomBeingCreated.start(this._hsApi, log); }); - return localId; + return roomBeingCreated; }); } @@ -691,10 +691,9 @@ export class Session { if (roomBeingCreated.roomId === roomId) { const observableStatus = this._observedRoomStatus.get(roomBeingCreated.localId); if (observableStatus) { - this._platform.logger.wrapOrRun(log, `replacing room being created`, log => { - log.set("localId", roomBeingCreated.localId) - .set("roomId", roomBeingCreated.roomId); - }); + log.log(`replacing room being created`) + .set("localId", roomBeingCreated.localId) + .set("roomId", roomBeingCreated.roomId); observableStatus.set(observableStatus.get() | RoomStatus.Replaced); } this._roomsBeingCreated.remove(roomBeingCreated.localId); @@ -737,10 +736,12 @@ export class Session { for (const is of inviteStates) { const statusObservable = this._observedRoomStatus.get(is.id); if (statusObservable) { + const withInvited = statusObservable.get() | RoomStatus.Invited; if (is.shouldAdd) { - statusObservable.set(statusObservable.get().withInvited()); + statusObservable.set(withInvited); } else if (is.shouldRemove) { - statusObservable.set(statusObservable.get().withoutInvited()); + const withoutInvited = withInvited ^ RoomStatus.Invited; + statusObservable.set(withoutInvited); } } } @@ -750,7 +751,7 @@ export class Session { _forgetArchivedRoom(roomId) { const statusObservable = this._observedRoomStatus.get(roomId); if (statusObservable) { - statusObservable.set(statusObservable.get() ^ RoomStatus.Archived); + statusObservable.set((statusObservable.get() | RoomStatus.Archived) ^ RoomStatus.Archived); } } diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index c42da436..83b68d3f 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -65,6 +65,7 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { public readonly isEncrypted: boolean; private _name: string; + private _error?: Error; constructor( public readonly localId: string, @@ -115,12 +116,12 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { if (this.isEncrypted) { options.initial_state = [createRoomEncryptionEvent()]; } - console.log("going to create the room now"); - const response = await hsApi.createRoom(options, {log}).response(); - this._roomId = response["room_id"]; - console.log("done creating the room now", this._roomId); - // TODO: somehow check in Session if we need to replace this with a joined room - // in case the room appears first in sync, and this request returns later + try { + const response = await hsApi.createRoom(options, {log}).response(); + this._roomId = response["room_id"]; + } catch (err) { + this._error = err; + } this.emitChange(undefined, log); } @@ -128,39 +129,30 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { // only load profiles if we need it for the room name and avatar if (!this.explicitName && this.inviteUserIds) { this.profiles = await loadProfiles(this.inviteUserIds, hsApi, log); - console.log("loaded the profiles", this.profiles); const summaryData = { joinCount: 1, // ourselves inviteCount: this.inviteUserIds.length }; this._name = calculateRoomName(this.profiles, summaryData, log); - console.log("loaded the profiles and the new name", this.name); this.emitChange(); } } - private emitChange(params?, log?) { + private emitChange(params?, log?: ILogItem) { this.updateCallback(this, params, log); this.emit("change"); } - get avatarColorId(): string { - return this.inviteUserIds?.[0] ?? this._roomId ?? this.localId; - } - - get avatarUrl(): string | undefined { - const result = this.profiles[0]?.avatarUrl; - console.log("RoomBeingCreated.avatarUrl", this.profiles, result); - return result; - } - - get roomId(): string | undefined { - return this._roomId; - } - + get avatarColorId(): string { return this.inviteUserIds?.[0] ?? this._roomId ?? this.localId; } + get avatarUrl(): string | undefined { return this.profiles[0]?.avatarUrl; } + get roomId(): string | undefined { return this._roomId; } get name() { return this._name; } - get isBeingCreated(): boolean { return true; } + get error(): Error | undefined { return this._error; } + + cancel() { + // remove from collection somehow + } } export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { @@ -194,5 +186,4 @@ class UserIdProfile implements IProfile { get displayName() { return undefined; } get name() { return this.userId; } get avatarUrl() { return undefined; } - } From 26fa2a5d601816963771c18001db5c84c7153c28 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 7 Feb 2022 18:58:43 +0100 Subject: [PATCH 119/160] add option --- src/matrix/Session.js | 8 ++++++-- src/matrix/room/create.ts | 16 +++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 02635855..13901e6c 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -592,13 +592,17 @@ export class Session { return this._roomsBeingCreated; } - createRoom(type, isEncrypted, explicitName, topic, invites, log = undefined) { + createRoom(type, isEncrypted, explicitName, topic, invites, options = undefined, log = undefined) { return this._platform.logger.wrapOrRun(log, "create room", log => { const localId = `local-${Math.round(this._platform.random() * Math.MAX_SAFE_INTEGER)}`; const roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, explicitName, topic, invites, this._roomsBeingCreatedUpdateCallback, this._mediaRepository, log); this._roomsBeingCreated.set(localId, roomBeingCreated); log.wrapDetached("create room network", log => { - return roomBeingCreated.start(this._hsApi, log); + const promises = [roomBeingCreated.create(this._hsApi, log)]; + if (options?.loadProfiles) { + promises.push(roomBeingCreated.loadProfiles(this._hsApi, log)); + } + return Promise.all(promises); }); return roomBeingCreated; }); diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index 83b68d3f..66714d72 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -92,14 +92,7 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { } } - public async start(hsApi: HomeServerApi, log: ILogItem): Promise { - await Promise.all([ - this.loadProfiles(hsApi, log), - this.create(hsApi, log), - ]); - } - - private async create(hsApi: HomeServerApi, log: ILogItem): Promise { + async create(hsApi: HomeServerApi, log: ILogItem): Promise { const options: CreateRoomPayload = { is_direct: this.type === RoomType.DirectMessage, preset: presetForType(this.type) @@ -125,7 +118,12 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { this.emitChange(undefined, log); } - private async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise { + /** requests the profiles of the invitees if needed to give an accurate + * estimated room name in case an explicit room name is not set. + * The room is being created in the background whether this is called + * or not, and this just gives a more accurate name while that request + * is running. */ + async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise { // only load profiles if we need it for the room name and avatar if (!this.explicitName && this.inviteUserIds) { this.profiles = await loadProfiles(this.inviteUserIds, hsApi, log); From e04463c143c4ed9e266f4b70a5b9d4fdeff63e4a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 7 Feb 2022 18:58:53 +0100 Subject: [PATCH 120/160] WIP for finding DM room --- .../session/rightpanel/MemberDetailsViewModel.js | 10 ++++++++-- src/matrix/Session.js | 13 +++++++++++++ src/matrix/room/BaseRoom.js | 4 ++++ src/matrix/room/Invite.js | 4 ++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index b0573bfa..f56aac9a 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -83,7 +83,13 @@ export class MemberDetailsViewModel extends ViewModel { } async openDirectMessage() { - const roomBeingCreated = await this._session.createRoom(RoomType.DirectMessage, undefined, undefined, undefined, [this.userId]); - this.navigation.push("room", roomBeingCreated.localId); + const room = this._session.findDirectMessageForUserId(this.userId); + let roomId = room?.id; + if (!roomId) { + const roomBeingCreated = await this._session.createRoom( + RoomType.DirectMessage, undefined, undefined, undefined, [this.userId], {loadProfiles: true}); + roomId = roomBeingCreated.localId; + } + this.navigation.push("room", roomId); } } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 13901e6c..45cd9b19 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -537,6 +537,19 @@ export class Session { return this._rooms; } + findDirectMessageForUserId(userId) { + for (const [,room] of this._rooms) { + if (room.isDirectMessageForUserId(userId)) { + return room; + } + } + for (const [,invite] of this._invites) { + if (invite.isDirectMessageForUserId(userId)) { + return invite; + } + } + } + /** @internal */ createJoinedRoom(roomId, pendingEvents) { return new Room({ diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index ecd2d860..446c22d1 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -420,6 +420,10 @@ export class BaseRoom extends EventEmitter { return this._summary.data.membership; } + isDirectMessageForUserId(userId) { + return this._summary.data.dmUserId === userId; + } + async _loadPowerLevels() { const txn = await this._storage.readTxn([this._storage.storeNames.roomState]); const powerLevelsState = await txn.roomState.get(this._roomId, "m.room.power_levels", ""); diff --git a/src/matrix/room/Invite.js b/src/matrix/room/Invite.js index da721c77..18bb7b8d 100644 --- a/src/matrix/room/Invite.js +++ b/src/matrix/room/Invite.js @@ -73,6 +73,10 @@ export class Invite extends EventEmitter { return this._inviter; } + isDirectMessageForUserId(userId) { + return this.isDirectMessage && this._inviter.userId === userId; + } + get isPublic() { return this._inviteData.joinRule === "public"; } From 45c8e3a793cef9b207bc7e834ad45f2073ae632e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 8 Feb 2022 14:34:34 +0100 Subject: [PATCH 121/160] mark room as DM based on synced state events,rather than just inviteData as that does not work for rooms we create ourselves --- src/matrix/Sync.js | 13 +++++----- src/matrix/room/Invite.js | 8 ++---- src/matrix/room/Room.js | 7 ++---- src/matrix/room/RoomSummary.js | 46 ++++++++++++++++++---------------- 4 files changed, 34 insertions(+), 40 deletions(-) diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index 5eb50b5c..3574213e 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -249,7 +249,7 @@ export class Sync { if (!isRoomInResponse) { let room = this._session.rooms.get(roomId); if (room) { - roomStates.push(new RoomSyncProcessState(room, false, null, {}, room.membership)); + roomStates.push(new RoomSyncProcessState(room, false, {}, room.membership)); } } } @@ -264,7 +264,7 @@ export class Sync { await rs.room.load(null, prepareTxn, log); } return rs.room.prepareSync( - rs.roomResponse, rs.membership, rs.invite, newKeys, prepareTxn, log) + rs.roomResponse, rs.membership, newKeys, prepareTxn, log) }, log.level.Detail); })); @@ -366,7 +366,7 @@ export class Sync { if (invite) { inviteStates.push(new InviteSyncProcessState(invite, false, null, membership)); } - const roomState = this._createRoomSyncState(roomId, invite, roomResponse, membership, isInitialSync); + const roomState = this._createRoomSyncState(roomId, roomResponse, membership, isInitialSync); if (roomState) { roomStates.push(roomState); } @@ -381,7 +381,7 @@ export class Sync { return {roomStates, archivedRoomStates}; } - _createRoomSyncState(roomId, invite, roomResponse, membership, isInitialSync) { + _createRoomSyncState(roomId, roomResponse, membership, isInitialSync) { let isNewRoom = false; let room = this._session.rooms.get(roomId); // create room only either on new join, @@ -397,7 +397,7 @@ export class Sync { } if (room) { return new RoomSyncProcessState( - room, isNewRoom, invite, roomResponse, membership); + room, isNewRoom, roomResponse, membership); } } @@ -468,10 +468,9 @@ class SessionSyncProcessState { } class RoomSyncProcessState { - constructor(room, isNewRoom, invite, roomResponse, membership) { + constructor(room, isNewRoom, roomResponse, membership) { this.room = room; this.isNewRoom = isNewRoom; - this.invite = invite; this.roomResponse = roomResponse; this.membership = membership; this.preparation = null; diff --git a/src/matrix/room/Invite.js b/src/matrix/room/Invite.js index 18bb7b8d..6c5ef121 100644 --- a/src/matrix/room/Invite.js +++ b/src/matrix/room/Invite.js @@ -188,7 +188,7 @@ export class Invite extends EventEmitter { return { roomId: this.id, isEncrypted: !!summaryData.encryption, - isDirectMessage: this._isDirectMessage(myInvite), + isDirectMessage: summaryData.isDirectMessage, // type: name, avatarUrl, @@ -200,12 +200,8 @@ export class Invite extends EventEmitter { }; } - _isDirectMessage(myInvite) { - return !!(myInvite?.content?.is_direct); - } - _createSummaryData(inviteState) { - return inviteState.reduce(processStateEvent, new SummaryData(null, this.id)); + return inviteState.reduce((data, event) => processStateEvent(data, event, this._user.id), new SummaryData(null, this.id)); } async _createHeroes(inviteState, log) { diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 46c06af0..65cfe693 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -54,15 +54,12 @@ export class Room extends BaseRoom { return false; } - async prepareSync(roomResponse, membership, invite, newKeys, txn, log) { + async prepareSync(roomResponse, membership, newKeys, txn, log) { log.set("id", this.id); if (newKeys) { log.set("newKeys", newKeys.length); } - let summaryChanges = this._summary.data.applySyncResponse(roomResponse, membership); - if (membership === "join" && invite) { - summaryChanges = summaryChanges.applyInvite(invite); - } + let summaryChanges = this._summary.data.applySyncResponse(roomResponse, membership, this._user.id); let roomEncryption = this._roomEncryption; // encryption is enabled in this sync if (!roomEncryption && summaryChanges.encryption) { diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index 82087200..410352ca 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -46,7 +46,7 @@ export function reduceStateEvents(roomResponse, callback, value) { return value; } -function applySyncResponse(data, roomResponse, membership) { +function applySyncResponse(data, roomResponse, membership, ownUserId) { if (roomResponse.summary) { data = updateSummary(data, roomResponse.summary); } @@ -60,7 +60,7 @@ function applySyncResponse(data, roomResponse, membership) { // process state events in state and in timeline. // non-state events are handled by applyTimelineEntries // so decryption is handled properly - data = reduceStateEvents(roomResponse, processStateEvent, data); + data = reduceStateEvents(roomResponse, (data, event) => processStateEvent(data, event, ownUserId), data); const unreadNotifications = roomResponse.unread_notifications; if (unreadNotifications) { data = processNotificationCounts(data, unreadNotifications); @@ -95,7 +95,7 @@ function processRoomAccountData(data, event) { return data; } -export function processStateEvent(data, event) { +export function processStateEvent(data, event, ownUserId) { if (event.type === "m.room.create") { data = data.cloneIfNeeded(); data.lastMessageTimestamp = event.origin_server_ts; @@ -121,6 +121,25 @@ export function processStateEvent(data, event) { const content = event.content; data = data.cloneIfNeeded(); data.canonicalAlias = content.alias; + } else if (event.type === "m.room.member") { + const content = event.content; + if (content.is_direct === true && content.membership === "invite" && !data.isDirectMessage) { + let other; + if (event.sender === ownUserId) { + other = event.state_key; + } else if (event.state_key === ownUserId) { + other = event.sender; + } + if (other) { + data = data.cloneIfNeeded(); + data.isDirectMessage = true; + data.dmUserId = other; + } + } else if (content.membership === "leave" && data.isDirectMessage && data.dmUserId === event.state_key) { + data = data.cloneIfNeeded(); + data.isDirectMessage = false; + data.dmUserId = null; + } } return data; } @@ -161,19 +180,6 @@ function updateSummary(data, summary) { return data; } -function applyInvite(data, invite) { - if (data.isDirectMessage !== invite.isDirectMessage) { - data = data.cloneIfNeeded(); - data.isDirectMessage = invite.isDirectMessage; - if (invite.isDirectMessage) { - data.dmUserId = invite.inviter?.userId; - } else { - data.dmUserId = null; - } - } - return data; -} - export class SummaryData { constructor(copy, roomId) { this.roomId = copy ? copy.roomId : roomId; @@ -230,12 +236,8 @@ export class SummaryData { return applyTimelineEntries(this, timelineEntries, isInitialSync, canMarkUnread, ownUserId); } - applySyncResponse(roomResponse, membership) { - return applySyncResponse(this, roomResponse, membership); - } - - applyInvite(invite) { - return applyInvite(this, invite); + applySyncResponse(roomResponse, membership, ownUserId) { + return applySyncResponse(this, roomResponse, membership, ownUserId); } get needsHeroes() { From d7b024eac11cb3beb577a1e96958ff95888bcdf3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 8 Feb 2022 14:35:14 +0100 Subject: [PATCH 122/160] unrelated fix: encode user name in matrix.to link --- src/domain/session/rightpanel/MemberDetailsViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index f56aac9a..7ea2ead5 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -79,7 +79,7 @@ export class MemberDetailsViewModel extends ViewModel { } get linkToUser() { - return `https://matrix.to/#/${this._member.userId}`; + return `https://matrix.to/#/${encodeURIComponent(this._member.userId)}`; } async openDirectMessage() { From 5325b0b466bdb400b41ad940b9afa2b8c04482e9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 8 Feb 2022 14:58:29 +0100 Subject: [PATCH 123/160] cleanup logging --- .../rightpanel/MemberDetailsViewModel.js | 6 ++- src/matrix/Session.js | 38 ++++++++++--------- src/matrix/room/create.ts | 12 +++--- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index 7ea2ead5..68045eba 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -86,8 +86,10 @@ export class MemberDetailsViewModel extends ViewModel { const room = this._session.findDirectMessageForUserId(this.userId); let roomId = room?.id; if (!roomId) { - const roomBeingCreated = await this._session.createRoom( - RoomType.DirectMessage, undefined, undefined, undefined, [this.userId], {loadProfiles: true}); + const roomBeingCreated = await this._session.createRoom({ + type: RoomType.DirectMessage, + invites: [this.userId] + }); roomId = roomBeingCreated.localId; } this.navigation.push("room", roomId); diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 45cd9b19..8d1c8613 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -64,12 +64,7 @@ export class Session { this._activeArchivedRooms = new Map(); this._invites = new ObservableMap(); this._inviteUpdateCallback = (invite, params) => this._invites.update(invite.id, params); - this._roomsBeingCreatedUpdateCallback = (rbc, params, log) => { - this._roomsBeingCreated.update(rbc.localId, params); - if (rbc.roomId && !!this.rooms.get(rbc.roomId)) { - this._tryReplaceRoomBeingCreated(rbc.roomId, log); - } - }; + this._roomsBeingCreatedUpdateCallback = (rbc, params) => this._roomsBeingCreated.update(rbc.localId, params); this._roomsBeingCreated = new ObservableMap(); this._user = new User(sessionInfo.userId); this._deviceMessageHandler = new DeviceMessageHandler({storage}); @@ -605,20 +600,27 @@ export class Session { return this._roomsBeingCreated; } - createRoom(type, isEncrypted, explicitName, topic, invites, options = undefined, log = undefined) { - return this._platform.logger.wrapOrRun(log, "create room", log => { - const localId = `local-${Math.round(this._platform.random() * Math.MAX_SAFE_INTEGER)}`; - const roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, explicitName, topic, invites, this._roomsBeingCreatedUpdateCallback, this._mediaRepository, log); + createRoom({type, isEncrypted, explicitName, topic, invites, loadProfiles = true}, log = undefined) { + let roomBeingCreated; + this._platform.logger.runDetached("create room", async log => { + const localId = `local-${Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)}`; + roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, + explicitName, topic, invites, this._roomsBeingCreatedUpdateCallback, + this._mediaRepository, log); this._roomsBeingCreated.set(localId, roomBeingCreated); - log.wrapDetached("create room network", log => { - const promises = [roomBeingCreated.create(this._hsApi, log)]; - if (options?.loadProfiles) { - promises.push(roomBeingCreated.loadProfiles(this._hsApi, log)); - } - return Promise.all(promises); - }); - return roomBeingCreated; + const promises = [roomBeingCreated.create(this._hsApi, log)]; + if (loadProfiles) { + promises.push(roomBeingCreated.loadProfiles(this._hsApi, log)); + } + await Promise.all(promises); + // we should now know the roomId, check if the room was synced before we received + // the room id. Replace the room being created with the synced room. + if (roomBeingCreated.roomId && !!this.rooms.get(roomBeingCreated.roomId)) { + this._tryReplaceRoomBeingCreated(roomBeingCreated.roomId, log); + } + // TODO: if type is DM, then adjust the m.direct account data }); + return roomBeingCreated; } async obtainSyncLock(syncResponse) { diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index 66714d72..4bdae474 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -74,7 +74,7 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { private readonly explicitName: string | undefined, private readonly topic: string | undefined, private readonly inviteUserIds: string[] | undefined, - private readonly updateCallback, + private readonly updateCallback: (self: RoomBeingCreated, params: string | undefined) => void, public readonly mediaRepository: MediaRepository, log: ILogItem ) { @@ -92,6 +92,7 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { } } + /** @internal */ async create(hsApi: HomeServerApi, log: ILogItem): Promise { const options: CreateRoomPayload = { is_direct: this.type === RoomType.DirectMessage, @@ -115,7 +116,7 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { } catch (err) { this._error = err; } - this.emitChange(undefined, log); + this.emitChange(); } /** requests the profiles of the invitees if needed to give an accurate @@ -123,6 +124,7 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { * The room is being created in the background whether this is called * or not, and this just gives a more accurate name while that request * is running. */ + /** @internal */ async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise { // only load profiles if we need it for the room name and avatar if (!this.explicitName && this.inviteUserIds) { @@ -136,8 +138,8 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { } } - private emitChange(params?, log?: ILogItem) { - this.updateCallback(this, params, log); + private emitChange(params?: string) { + this.updateCallback(this, params); this.emit("change"); } @@ -149,7 +151,7 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { get error(): Error | undefined { return this._error; } cancel() { - // remove from collection somehow + // TODO: remove from collection somehow } } From 743f2270e5c2c07735fd769b345901a7daf70df0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 8 Feb 2022 16:22:44 +0100 Subject: [PATCH 124/160] have a single tile view that supports all 3 view models --- .../session/leftpanel/InviteTileViewModel.js | 27 +++------- .../ui/session/leftpanel/InviteTileView.js | 50 ------------------- .../web/ui/session/leftpanel/LeftPanelView.js | 9 +--- .../web/ui/session/leftpanel/RoomTileView.js | 19 ++++--- 4 files changed, 22 insertions(+), 83 deletions(-) delete mode 100644 src/platform/web/ui/session/leftpanel/InviteTileView.js diff --git a/src/domain/session/leftpanel/InviteTileViewModel.js b/src/domain/session/leftpanel/InviteTileViewModel.js index 10c84628..09c11fb8 100644 --- a/src/domain/session/leftpanel/InviteTileViewModel.js +++ b/src/domain/session/leftpanel/InviteTileViewModel.js @@ -25,17 +25,14 @@ export class InviteTileViewModel extends BaseTileViewModel { this._url = this.urlCreator.openRoomActionUrl(this._invite.id); } - get busy() { - return this._invite.accepting || this._invite.rejecting; - } - - get kind() { - return "invite"; - } - - get url() { - return this._url; - } + get busy() { return this._invite.accepting || this._invite.rejecting; } + get kind() { return "invite"; } + get url() { return this._url; } + get name() { return this._invite.name; } + get isHighlighted() { return true; } + get isUnread() { return true; } + get badgeCount() { return this.i18n`!`; } + get _avatarSource() { return this._invite; } compare(other) { const parentComparison = super.compare(other); @@ -44,12 +41,4 @@ export class InviteTileViewModel extends BaseTileViewModel { } return other._invite.timestamp - this._invite.timestamp; } - - get name() { - return this._invite.name; - } - - get _avatarSource() { - return this._invite; - } } diff --git a/src/platform/web/ui/session/leftpanel/InviteTileView.js b/src/platform/web/ui/session/leftpanel/InviteTileView.js deleted file mode 100644 index de064bed..00000000 --- a/src/platform/web/ui/session/leftpanel/InviteTileView.js +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2020 Bruno Windels -Copyright 2020, 2021 The Matrix.org Foundation C.I.C. - -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. -*/ - -import {TemplateView} from "../../general/TemplateView"; -import {AvatarView} from "../../AvatarView"; -import {spinner} from "../../common.js"; - -export class InviteTileView extends TemplateView { - render(t, vm) { - const classes = { - "active": vm => vm.isOpen, - "hidden": vm => vm.hidden - }; - return t.li({"className": classes}, [ - t.a({href: vm.url}, [ - t.view(new AvatarView(vm, 32), {parentProvidesUpdates: true}), - t.div({className: "description"}, [ - t.div({className: "name"}, vm => vm.name), - t.map(vm => vm.busy, busy => { - if (busy) { - return spinner(t); - } else { - return t.div({className: "badge highlighted"}, "!"); - } - }) - ]) - ]) - ]); - } - - update(value, props) { - super.update(value); - // update the AvatarView as we told it to not subscribe itself with parentProvidesUpdates - this.updateSubViews(value, props); - } -} diff --git a/src/platform/web/ui/session/leftpanel/LeftPanelView.js b/src/platform/web/ui/session/leftpanel/LeftPanelView.js index bc14fa1e..1b3c2ae7 100644 --- a/src/platform/web/ui/session/leftpanel/LeftPanelView.js +++ b/src/platform/web/ui/session/leftpanel/LeftPanelView.js @@ -17,7 +17,6 @@ limitations under the License. import {ListView} from "../../general/ListView"; import {TemplateView} from "../../general/TemplateView"; import {RoomTileView} from "./RoomTileView.js"; -import {InviteTileView} from "./InviteTileView.js"; class FilterField extends TemplateView { render(t, options) { @@ -63,13 +62,7 @@ export class LeftPanelView extends TemplateView { className: "RoomList", list: vm.tileViewModels, }, - tileVM => { - if (tileVM.kind === "invite" || tileVM.kind === "roomBeingCreated") { - return new InviteTileView(tileVM); - } else { - return new RoomTileView(tileVM); - } - } + tileVM => new RoomTileView(tileVM) )); const utilitiesRow = t.div({className: "utilities"}, [ t.a({className: "button-utility close-session", href: vm.closeUrl, "aria-label": vm.i18n`Back to account list`, title: vm.i18n`Back to account list`}), diff --git a/src/platform/web/ui/session/leftpanel/RoomTileView.js b/src/platform/web/ui/session/leftpanel/RoomTileView.js index 28957541..4875c167 100644 --- a/src/platform/web/ui/session/leftpanel/RoomTileView.js +++ b/src/platform/web/ui/session/leftpanel/RoomTileView.js @@ -17,6 +17,7 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView"; import {AvatarView} from "../../AvatarView.js"; +import {spinner} from "../../common.js"; export class RoomTileView extends TemplateView { render(t, vm) { @@ -29,13 +30,19 @@ export class RoomTileView extends TemplateView { t.view(new AvatarView(vm, 32), {parentProvidesUpdates: true}), t.div({className: "description"}, [ t.div({className: {"name": true, unread: vm => vm.isUnread}}, vm => vm.name), - t.div({ - className: { - badge: true, - highlighted: vm => vm.isHighlighted, - hidden: vm => !vm.badgeCount + t.map(vm => vm.busy, busy => { + if (busy) { + return spinner(t); + } else { + return t.div({ + className: { + badge: true, + highlighted: vm => vm.isHighlighted, + hidden: vm => !vm.badgeCount + } + }, vm => vm.badgeCount); } - }, vm => vm.badgeCount), + }) ]) ]) ]); From afe8e17a6fc9c506239235bd0009a46b6226a79c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 8 Feb 2022 17:00:06 +0100 Subject: [PATCH 125/160] remove debugging code --- src/matrix/Session.js | 9 +-------- src/matrix/room/Room.js | 1 - 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 8d1c8613..4bb88f4e 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -886,7 +886,7 @@ export class Session { let observable = this._observedRoomStatus.get(roomId); if (!observable) { const status = await this.getRoomStatus(roomId); - observable = new FooRetainedObservableValue(status, () => { + observable = new RetainedObservableValue(status, () => { this._observedRoomStatus.delete(roomId); }); @@ -941,13 +941,6 @@ export class Session { } } -class FooRetainedObservableValue extends RetainedObservableValue { - set(value) { - console.log("setting room status to", value); - super.set(value); - } -} - export function tests() { function createStorageMock(session, pendingEvents = []) { return { diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 65cfe693..a5bce14f 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -228,7 +228,6 @@ export class Room extends BaseRoom { } } let emitChange = false; - console.log("Room summaryChanges", this.id, summaryChanges); if (summaryChanges) { this._summary.applyChanges(summaryChanges); if (!this._summary.data.needsHeroes) { From 83d2b58badaee3218821b215f695f46d0a1d91ea Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Feb 2022 18:58:30 +0100 Subject: [PATCH 126/160] add avatar support to creating room --- .../RoomBeingCreatedTileViewModel.js | 5 + src/matrix/Session.js | 10 +- src/matrix/room/AttachmentUpload.js | 8 +- src/matrix/room/create.ts | 115 +++++++++++++----- src/platform/types/types.ts | 14 +++ 5 files changed, 111 insertions(+), 41 deletions(-) diff --git a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js index 8c629892..d7840bfa 100644 --- a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js +++ b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js @@ -50,4 +50,9 @@ export class RoomBeingCreatedTileViewModel extends BaseTileViewModel { get _avatarSource() { return this._roomBeingCreated; } + + avatarUrl(size) { + // allow blob url which doesn't need mxc => http resolution + return this._roomBeingCreated.avatarBlobUrl ?? super.avatarUrl(size); + } } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 4bb88f4e..b03306ed 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -600,15 +600,16 @@ export class Session { return this._roomsBeingCreated; } - createRoom({type, isEncrypted, explicitName, topic, invites, loadProfiles = true}, log = undefined) { + createRoom(options, log = undefined) { let roomBeingCreated; this._platform.logger.runDetached("create room", async log => { const localId = `local-${Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)}`; - roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, - explicitName, topic, invites, this._roomsBeingCreatedUpdateCallback, - this._mediaRepository, log); + roomBeingCreated = new RoomBeingCreated( + localId, options, this._roomsBeingCreatedUpdateCallback, + this._mediaRepository, this._platform, log); this._roomsBeingCreated.set(localId, roomBeingCreated); const promises = [roomBeingCreated.create(this._hsApi, log)]; + const loadProfiles = !(options.loadProfiles === false); // default to true if (loadProfiles) { promises.push(roomBeingCreated.loadProfiles(this._hsApi, log)); } @@ -715,6 +716,7 @@ export class Session { .set("roomId", roomBeingCreated.roomId); observableStatus.set(observableStatus.get() | RoomStatus.Replaced); } + roomBeingCreated.dispose(); this._roomsBeingCreated.remove(roomBeingCreated.localId); return; } diff --git a/src/matrix/room/AttachmentUpload.js b/src/matrix/room/AttachmentUpload.js index e2e7e3bf..6cf4c1db 100644 --- a/src/matrix/room/AttachmentUpload.js +++ b/src/matrix/room/AttachmentUpload.js @@ -40,17 +40,15 @@ export class AttachmentUpload { return this._sentBytes; } - /** @public */ abort() { this._uploadRequest?.abort(); } - /** @public */ get localPreview() { return this._unencryptedBlob; } - /** @package */ + /** @internal */ async encrypt() { if (this._encryptionInfo) { throw new Error("already encrypted"); @@ -60,7 +58,7 @@ export class AttachmentUpload { this._encryptionInfo = info; } - /** @package */ + /** @internal */ async upload(hsApi, progressCallback, log) { this._uploadRequest = hsApi.uploadAttachment(this._transferredBlob, this._filename, { uploadProgress: sentBytes => { @@ -73,7 +71,7 @@ export class AttachmentUpload { this._mxcUrl = content_uri; } - /** @package */ + /** @internal */ applyToContent(urlPath, content) { if (!this._mxcUrl) { throw new Error("upload has not finished"); diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index 4bdae474..e3a1ed4d 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -18,10 +18,12 @@ import {calculateRoomName} from "./members/Heroes"; import {createRoomEncryptionEvent} from "../e2ee/common"; import {MediaRepository} from "../net/MediaRepository"; import {EventEmitter} from "../../utils/EventEmitter"; +import {AttachmentUpload} from "./AttachmentUpload"; -import type {StateEvent} from "../storage/types"; import type {HomeServerApi} from "../net/HomeServerApi"; import type {ILogItem} from "../../logging/types"; +import type {Platform} from "../../platform/web/Platform"; +import type {IBlobHandle} from "../../platform/types/types"; type CreateRoomPayload = { is_direct?: boolean; @@ -29,7 +31,31 @@ type CreateRoomPayload = { name?: string; topic?: string; invite?: string[]; - initial_state?: StateEvent[] + room_alias_name?: string; + initial_state: {type: string; state_key: string; content: Record}[] +} + +type ImageInfo = { + w: number; + h: number; + mimetype: string; + size: number; +} + +type Avatar = { + info: ImageInfo; + blob: IBlobHandle; + name: string; +} + +type Options = { + type: RoomType; + isEncrypted?: boolean; + name?: string; + topic?: string; + invites?: string[]; + avatar?: Avatar; + alias?: string; } export enum RoomType { @@ -64,54 +90,72 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { private profiles: Profile[] = []; public readonly isEncrypted: boolean; - private _name: string; + private _calculatedName: string; private _error?: Error; constructor( public readonly localId: string, - private readonly type: RoomType, - isEncrypted: boolean | undefined, - private readonly explicitName: string | undefined, - private readonly topic: string | undefined, - private readonly inviteUserIds: string[] | undefined, + private readonly options: Options, private readonly updateCallback: (self: RoomBeingCreated, params: string | undefined) => void, public readonly mediaRepository: MediaRepository, + public readonly platform: Platform, log: ILogItem ) { super(); - this.isEncrypted = isEncrypted === undefined ? defaultE2EEStatusForType(this.type) : isEncrypted; - if (explicitName) { - this._name = explicitName; + this.isEncrypted = options.isEncrypted === undefined ? defaultE2EEStatusForType(options.type) : options.isEncrypted; + if (options.name) { + this._calculatedName = options.name; } else { const summaryData = { joinCount: 1, // ourselves - inviteCount: (this.inviteUserIds?.length || 0) + inviteCount: (options.invites?.length || 0) }; - const userIdProfiles = (inviteUserIds || []).map(userId => new UserIdProfile(userId)); - this._name = calculateRoomName(userIdProfiles, summaryData, log); + const userIdProfiles = (options.invites || []).map(userId => new UserIdProfile(userId)); + this._calculatedName = calculateRoomName(userIdProfiles, summaryData, log); } } /** @internal */ async create(hsApi: HomeServerApi, log: ILogItem): Promise { - const options: CreateRoomPayload = { - is_direct: this.type === RoomType.DirectMessage, - preset: presetForType(this.type) + let avatarEventContent; + if (this.options.avatar) { + const {avatar} = this.options; + const attachment = new AttachmentUpload({filename: avatar.name, blob: avatar.blob, platform: this.platform}); + await attachment.upload(hsApi, () => {}, log); + avatarEventContent = { + info: avatar.info + }; + attachment.applyToContent("url", avatarEventContent); + } + const createOptions: CreateRoomPayload = { + is_direct: this.options.type === RoomType.DirectMessage, + preset: presetForType(this.options.type), + initial_state: [] }; - if (this.explicitName) { - options.name = this.explicitName; + if (this.options.name) { + createOptions.name = this.options.name; } - if (this.topic) { - options.topic = this.topic; + if (this.options.topic) { + createOptions.topic = this.options.topic; } - if (this.inviteUserIds) { - options.invite = this.inviteUserIds; + if (this.options.invites) { + createOptions.invite = this.options.invites; + } + if (this.options.alias) { + createOptions.room_alias_name = this.options.alias; } if (this.isEncrypted) { - options.initial_state = [createRoomEncryptionEvent()]; + createOptions.initial_state.push(createRoomEncryptionEvent()); + } + if (avatarEventContent) { + createOptions.initial_state.push({ + type: "m.room.avatar", + state_key: "", + content: avatarEventContent + }); } try { - const response = await hsApi.createRoom(options, {log}).response(); + const response = await hsApi.createRoom(createOptions, {log}).response(); this._roomId = response["room_id"]; } catch (err) { this._error = err; @@ -127,13 +171,13 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { /** @internal */ async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise { // only load profiles if we need it for the room name and avatar - if (!this.explicitName && this.inviteUserIds) { - this.profiles = await loadProfiles(this.inviteUserIds, hsApi, log); + if (!this.options.name && this.options.invites) { + this.profiles = await loadProfiles(this.options.invites, hsApi, log); const summaryData = { joinCount: 1, // ourselves - inviteCount: this.inviteUserIds.length + inviteCount: this.options.invites.length }; - this._name = calculateRoomName(this.profiles, summaryData, log); + this._calculatedName = calculateRoomName(this.profiles, summaryData, log); this.emitChange(); } } @@ -143,16 +187,23 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { this.emit("change"); } - get avatarColorId(): string { return this.inviteUserIds?.[0] ?? this._roomId ?? this.localId; } - get avatarUrl(): string | undefined { return this.profiles[0]?.avatarUrl; } + get avatarColorId(): string { return this.options.invites?.[0] ?? this._roomId ?? this.localId; } + get avatarUrl(): string | undefined { return this.profiles?.[0].avatarUrl; } + get avatarBlobUrl(): string | undefined { return this.options.avatar?.blob?.url; } get roomId(): string | undefined { return this._roomId; } - get name() { return this._name; } + get name() { return this._calculatedName; } get isBeingCreated(): boolean { return true; } get error(): Error | undefined { return this._error; } cancel() { // TODO: remove from collection somehow } + + dispose() { + if (this.options.avatar) { + this.options.avatar.blob.dispose(); + } + } } export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts index 58d2216f..91884117 100644 --- a/src/platform/types/types.ts +++ b/src/platform/types/types.ts @@ -31,3 +31,17 @@ export interface IRequestOptions { } export type RequestFunction = (url: string, options: IRequestOptions) => RequestResult; + +export interface IBlobHandle { + nativeBlob: any; + url: string; + size: number; + mimeType: string; + readAsBuffer(): BufferSource; + dispose() +} + +export type File = { + readonly name: string; + readonly blob: IBlobHandle; +} From 8523f6feafa7ecdeb1ea3a19131d34011aab5c0c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Feb 2022 19:00:41 +0100 Subject: [PATCH 127/160] setup navigation for create room form --- src/domain/navigation/index.js | 2 +- src/domain/session/SessionViewModel.js | 27 ++++++++++++++++--- .../session/leftpanel/LeftPanelViewModel.js | 3 +++ src/platform/web/ui/session/SessionView.js | 3 +++ .../web/ui/session/leftpanel/LeftPanelView.js | 1 + 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index e209f2e5..086367ce 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -32,7 +32,7 @@ function allowsChild(parent, child) { // allowed root segments return type === "login" || type === "session" || type === "sso" || type === "logout"; case "session": - return type === "room" || type === "rooms" || type === "settings"; + return type === "room" || type === "rooms" || type === "settings" || type === "create-room"; case "rooms": // downside of the approach: both of these will control which tile is selected return type === "room" || type === "empty-grid-tile"; diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 11126ca9..5a818c4d 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -24,6 +24,7 @@ import {LightboxViewModel} from "./room/LightboxViewModel.js"; import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {RoomGridViewModel} from "./RoomGridViewModel.js"; import {SettingsViewModel} from "./settings/SettingsViewModel.js"; +import {CreateRoomViewModel} from "./CreateRoomViewModel.js"; import {ViewModel} from "../ViewModel.js"; import {RoomViewModelObservable} from "./RoomViewModelObservable.js"; import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js"; @@ -42,6 +43,7 @@ export class SessionViewModel extends ViewModel { this._settingsViewModel = null; this._roomViewModelObservable = null; this._gridViewModel = null; + this._createRoomViewModel = null; this._setupNavigation(); } @@ -73,6 +75,12 @@ export class SessionViewModel extends ViewModel { })); this._updateSettings(settings.get()); + const createRoom = this.navigation.observe("create-room"); + this.track(createRoom.subscribe(createRoomOpen => { + this._updateCreateRoom(createRoomOpen); + })); + this._updateCreateRoom(createRoom.get()); + const lightbox = this.navigation.observe("lightbox"); this.track(lightbox.subscribe(eventId => { this._updateLightbox(eventId); @@ -94,7 +102,7 @@ export class SessionViewModel extends ViewModel { } get activeMiddleViewModel() { - return this._roomViewModelObservable?.get() || this._gridViewModel || this._settingsViewModel; + return this._roomViewModelObservable?.get() || this._gridViewModel || this._settingsViewModel || this._createRoomViewModel; } get roomGridViewModel() { @@ -117,11 +125,14 @@ export class SessionViewModel extends ViewModel { return this._roomViewModelObservable?.get(); } - get rightPanelViewModel() { return this._rightPanelViewModel; } + get createRoomViewModel() { + return this._createRoomViewModel; + } + _updateGrid(roomIds) { const changed = !(this._gridViewModel && roomIds); const currentRoomId = this.navigation.path.get("room"); @@ -160,7 +171,7 @@ export class SessionViewModel extends ViewModel { } } - _createRoomViewModel(roomId) { + _createRoomViewModelInstance(roomId) { const room = this._client.session.rooms.get(roomId); if (room) { const roomVM = new RoomViewModel(this.childOptions({room})); @@ -246,6 +257,16 @@ export class SessionViewModel extends ViewModel { this.emitChange("activeMiddleViewModel"); } + _updateCreateRoom(createRoomOpen) { + if (this._createRoomViewModel) { + this._createRoomViewModel = this.disposeTracked(this._createRoomViewModel); + } + if (createRoomOpen) { + this._createRoomViewModel = this.track(new CreateRoomViewModel(this.childOptions({session: this._client.session}))); + } + this.emitChange("activeMiddleViewModel"); + } + _updateLightbox(eventId) { if (this._lightboxViewModel) { this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel); diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 597c0da6..cc3a2ccb 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -35,6 +35,7 @@ export class LeftPanelViewModel extends ViewModel { this._setupNavigation(); this._closeUrl = this.urlCreator.urlForSegment("session"); this._settingsUrl = this.urlCreator.urlForSegment("settings"); + this._createRoomUrl = this.urlCreator.urlForSegment("create-room"); } _mapTileViewModels(roomsBeingCreated, invites, rooms) { @@ -77,6 +78,8 @@ export class LeftPanelViewModel extends ViewModel { return this._settingsUrl; } + get createRoomUrl() { return this._createRoomUrl; } + _setupNavigation() { const roomObservable = this.navigation.observe("room"); this.track(roomObservable.subscribe(roomId => this._open(roomId))); diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index 3740ffca..e7cc406a 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -26,6 +26,7 @@ import {StaticView} from "../general/StaticView.js"; import {SessionStatusView} from "./SessionStatusView.js"; import {RoomGridView} from "./RoomGridView.js"; import {SettingsView} from "./settings/SettingsView.js"; +import {CreateRoomView} from "./CreateRoomView.js"; import {RightPanelView} from "./rightpanel/RightPanelView.js"; export class SessionView extends TemplateView { @@ -44,6 +45,8 @@ export class SessionView extends TemplateView { return new RoomGridView(vm.roomGridViewModel); } else if (vm.settingsViewModel) { return new SettingsView(vm.settingsViewModel); + } else if (vm.createRoomViewModel) { + return new CreateRoomView(vm.createRoomViewModel); } else if (vm.currentRoomViewModel) { if (vm.currentRoomViewModel.kind === "invite") { return new InviteView(vm.currentRoomViewModel); diff --git a/src/platform/web/ui/session/leftpanel/LeftPanelView.js b/src/platform/web/ui/session/leftpanel/LeftPanelView.js index 1b3c2ae7..c79192be 100644 --- a/src/platform/web/ui/session/leftpanel/LeftPanelView.js +++ b/src/platform/web/ui/session/leftpanel/LeftPanelView.js @@ -90,6 +90,7 @@ export class LeftPanelView extends TemplateView { "aria-label": gridButtonLabel }), t.a({className: "button-utility settings", href: vm.settingsUrl, "aria-label": vm.i18n`Settings`, title: vm.i18n`Settings`}), + t.a({className: "button-utility create", href: vm.createRoomUrl, "aria-label": vm.i18n`Create room`, title: vm.i18n`Create room`}), ]); return t.div({className: "LeftPanel"}, [ From 4b1be30dc0ef076c11c55520be08b210bf6d2283 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Feb 2022 19:01:35 +0100 Subject: [PATCH 128/160] improve form-row classes so they can work with create room form --- src/platform/web/ui/css/form.css | 2 +- src/platform/web/ui/css/layout.css | 11 +++++++++++ .../web/ui/css/themes/element/theme.css | 18 ++++++++++++++++-- src/platform/web/ui/login/PasswordLoginView.js | 4 ++-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/platform/web/ui/css/form.css b/src/platform/web/ui/css/form.css index bdb47bec..f5452c65 100644 --- a/src/platform/web/ui/css/form.css +++ b/src/platform/web/ui/css/form.css @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.form input { +.form .form-row.text > input, .form .form-row.text > textarea { display: block; width: 100%; min-width: 0; diff --git a/src/platform/web/ui/css/layout.css b/src/platform/web/ui/css/layout.css index a85ab109..2b3a04ae 100644 --- a/src/platform/web/ui/css/layout.css +++ b/src/platform/web/ui/css/layout.css @@ -216,3 +216,14 @@ the layout viewport up without resizing it when the keyboard shows */ justify-content: center; align-items: center; } + +.vertical-layout { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; +} + +.vertical-layout > .stretch { + flex: 1 0 0; +} diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 5ed8b4c9..eae02ee8 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -80,11 +80,19 @@ limitations under the License. flex: 1 0 auto; } +.form textarea { + font-family: "Inter", sans-serif; +} + +.form-group { + margin: 24px 0; +} + .form-row { margin: 12px 0; } -.form-row input { +.form-row.text > input, .form-row.text > textarea { padding: 12px; border: 1px solid rgba(141, 151, 165, 0.15); border-radius: 8px; @@ -92,7 +100,13 @@ limitations under the License. font-size: 1em; } -.form-row label, .form-row input { +.form-row.check { + display: flex; + align-items: baseline; + gap: 4px; +} + +.form-row.text > label, .form-row.text > input { display: block; } diff --git a/src/platform/web/ui/login/PasswordLoginView.js b/src/platform/web/ui/login/PasswordLoginView.js index 4360cc0f..6f47555d 100644 --- a/src/platform/web/ui/login/PasswordLoginView.js +++ b/src/platform/web/ui/login/PasswordLoginView.js @@ -41,8 +41,8 @@ export class PasswordLoginView extends TemplateView { } }, [ t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), - t.div({ className: "form-row" }, [t.label({ for: "username" }, vm.i18n`Username`), username]), - t.div({ className: "form-row" }, [t.label({ for: "password" }, vm.i18n`Password`), password]), + t.div({ className: "form-row text" }, [t.label({ for: "username" }, vm.i18n`Username`), username]), + t.div({ className: "form-row text" }, [t.label({ for: "password" }, vm.i18n`Password`), password]), t.div({ className: "button-row" }, [ t.button({ className: "button-action primary", From a1e14c4eeca4b3841ace9e284fc82793de13a836 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Feb 2022 19:02:18 +0100 Subject: [PATCH 129/160] rename to not have conflict between method name and instance of CreateRoomViewModel --- src/domain/session/RoomViewModelObservable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/RoomViewModelObservable.js b/src/domain/session/RoomViewModelObservable.js index c16aaf83..02a9a60b 100644 --- a/src/domain/session/RoomViewModelObservable.js +++ b/src/domain/session/RoomViewModelObservable.js @@ -76,7 +76,7 @@ export class RoomViewModelObservable extends ObservableValue { } else if (status & RoomStatus.Invited) { return this._sessionViewModel._createInviteViewModel(this.id); } else if (status & RoomStatus.Joined) { - return this._sessionViewModel._createRoomViewModel(this.id); + return this._sessionViewModel._createRoomViewModelInstance(this.id); } else if (status & RoomStatus.Archived) { return await this._sessionViewModel._createArchivedRoomViewModel(this.id); } else { From 5c085efc10fc058efe2bfdf77753820ee62d55c5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 9 Feb 2022 19:02:51 +0100 Subject: [PATCH 130/160] create room view and view model --- src/domain/session/CreateRoomViewModel.js | 126 ++++++++++++++++++ src/domain/session/common.js | 24 ++++ src/domain/session/room/RoomViewModel.js | 14 +- src/platform/web/ui/css/avatar.css | 1 + src/platform/web/ui/css/layout.css | 4 +- .../web/ui/css/themes/element/icons/plus.svg | 3 + .../web/ui/css/themes/element/theme.css | 29 ++++ src/platform/web/ui/session/CreateRoomView.js | 105 +++++++++++++++ 8 files changed, 294 insertions(+), 12 deletions(-) create mode 100644 src/domain/session/CreateRoomViewModel.js create mode 100644 src/domain/session/common.js create mode 100644 src/platform/web/ui/css/themes/element/icons/plus.svg create mode 100644 src/platform/web/ui/session/CreateRoomView.js diff --git a/src/domain/session/CreateRoomViewModel.js b/src/domain/session/CreateRoomViewModel.js new file mode 100644 index 00000000..47656128 --- /dev/null +++ b/src/domain/session/CreateRoomViewModel.js @@ -0,0 +1,126 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +import {ViewModel} from "../ViewModel.js"; +import {imageToInfo} from "./common.js"; +import {RoomType} from "../../matrix/room/create"; + +export class CreateRoomViewModel extends ViewModel { + constructor(options) { + super(options); + const {session} = options; + this._session = session; + this._name = ""; + this._topic = ""; + this._isPublic = false; + this._isEncrypted = true; + this._avatarScaledBlob = undefined; + this._avatarFileName = undefined; + this._avatarInfo = undefined; + } + + setName(name) { + this._name = name; + this.emitChange("name"); + } + + get name() { return this._name; } + + setTopic(topic) { + this._topic = topic; + this.emitChange("topic"); + } + + get topic() { return this._topic; } + + setPublic(isPublic) { + this._isPublic = isPublic; + this.emitChange("isPublic"); + } + + get isPublic() { return this._isPublic; } + + setEncrypted(isEncrypted) { + this._isEncrypted = isEncrypted; + this.emitChange("isEncrypted"); + } + + get isEncrypted() { return this._isEncrypted; } + + get canCreate() { + return !!this.name; + } + + create() { + let avatar; + if (this._avatarScaledBlob) { + avatar = { + info: this._avatarInfo, + name: this._avatarFileName, + blob: this._avatarScaledBlob + } + } + const roomBeingCreated = this._session.createRoom({ + type: this.isPublic ? RoomType.Public : RoomType.Private, + name: this.name ?? undefined, + topic: this.topic ?? undefined, + isEncrypted: !this.isPublic && this._isEncrypted, + alias: this.isPublic ? this.roomAlias : undefined, + avatar, + invites: ["@bwindels:matrix.org"] + }); + this.navigation.push("room", roomBeingCreated.localId); + } + + + avatarUrl() { return this._avatarScaledBlob.url; } + get avatarTitle() { return this.name; } + get avatarLetter() { return ""; } + get avatarColorNumber() { return 0; } + get hasAvatar() { return !!this._avatarScaledBlob; } + get error() { return ""; } + + async selectAvatar() { + if (!this.platform.hasReadPixelPermission()) { + alert("Please allow canvas image data access, so we can scale your images down."); + return; + } + if (this._avatarScaledBlob) { + this._avatarScaledBlob.dispose(); + } + this._avatarScaledBlob = undefined; + this._avatarFileName = undefined; + this._avatarInfo = undefined; + + const file = await this.platform.openFile("image/*"); + if (!file || !file.blob.mimeType.startsWith("image/")) { + // allow to clear the avatar by not selecting an image + this.emitChange("hasAvatar"); + return; + } + let image = await this.platform.loadImage(file.blob); + const limit = 800; + if (image.maxDimension > limit) { + const scaledImage = await image.scale(limit); + image.dispose(); + image = scaledImage; + } + this._avatarScaledBlob = image.blob; + this._avatarInfo = imageToInfo(image); + this._avatarFileName = file.name; + this.emitChange("hasAvatar"); + } +} diff --git a/src/domain/session/common.js b/src/domain/session/common.js new file mode 100644 index 00000000..1ab275bb --- /dev/null +++ b/src/domain/session/common.js @@ -0,0 +1,24 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +export function imageToInfo(image) { + return { + w: image.width, + h: image.height, + mimetype: image.blob.mimeType, + size: image.blob.size + }; +} diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index c81c47ef..2d10c7ca 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -20,6 +20,7 @@ import {ComposerViewModel} from "./ComposerViewModel.js" import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; import {tilesCreator} from "./timeline/tilesCreator.js"; import {ViewModel} from "../../ViewModel.js"; +import {imageToInfo} from "../common.js"; export class RoomViewModel extends ViewModel { constructor(options) { @@ -273,7 +274,9 @@ export class RoomViewModel extends ViewModel { let image = await this.platform.loadImage(file.blob); const limit = await this.platform.settingsStorage.getInt("sentImageSizeLimit"); if (limit && image.maxDimension > limit) { - image = await image.scale(limit); + const scaledImage = await image.scale(limit); + image.dispose(); + image = scaledImage; } const content = { body: file.name, @@ -319,15 +322,6 @@ export class RoomViewModel extends ViewModel { } } -function imageToInfo(image) { - return { - w: image.width, - h: image.height, - mimetype: image.blob.mimeType, - size: image.blob.size - }; -} - function videoToInfo(video) { const info = imageToInfo(video); info.duration = video.duration; diff --git a/src/platform/web/ui/css/avatar.css b/src/platform/web/ui/css/avatar.css index 143ea899..2ee9ca0c 100644 --- a/src/platform/web/ui/css/avatar.css +++ b/src/platform/web/ui/css/avatar.css @@ -34,6 +34,7 @@ limitations under the License. .hydrogen .avatar img { width: 100%; height: 100%; + object-fit: cover; } /* work around postcss-css-variables limitations and repeat variable usage */ diff --git a/src/platform/web/ui/css/layout.css b/src/platform/web/ui/css/layout.css index 2b3a04ae..a0f42b01 100644 --- a/src/platform/web/ui/css/layout.css +++ b/src/platform/web/ui/css/layout.css @@ -49,7 +49,7 @@ main { grid-template: "status status" auto "left middle" 1fr / - 300px 1fr; + 320px 1fr; min-height: 0; min-width: 0; } @@ -58,7 +58,7 @@ main { grid-template: "status status status" auto "left middle right" 1fr / - 300px 1fr 300px; + 320px 1fr 300px; } /* resize and reposition session view to account for mobile Safari which shifts diff --git a/src/platform/web/ui/css/themes/element/icons/plus.svg b/src/platform/web/ui/css/themes/element/icons/plus.svg new file mode 100644 index 00000000..ea197223 --- /dev/null +++ b/src/platform/web/ui/css/themes/element/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index eae02ee8..3a0d34ba 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -171,6 +171,10 @@ a.button-action { background-image: url('icons/settings.svg'); } +.button-utility.create { + background-image: url('icons/plus.svg'); +} + .button-utility.grid.on { background-image: url('icons/disable-grid.svg'); } @@ -1089,3 +1093,28 @@ button.RoomDetailsView_row::after { display: flex; gap: 12px; } + +.CreateRoomView { + padding: 0 12px; + justify-self: center; + max-width: 400px; + width: 100%; + box-sizing: border-box; +} + +.CreateRoomView_selectAvatar { + border: none; + background: none; + cursor: pointer; +} + +.CreateRoomView_selectAvatarPlaceholder { + width: 64px; + height: 64px; + border-radius: 100%; + background-color: #e1e3e6; + background-image: url('icons/plus.svg'); + background-repeat: no-repeat; + background-position: center; + background-size: 36px; +} diff --git a/src/platform/web/ui/session/CreateRoomView.js b/src/platform/web/ui/session/CreateRoomView.js new file mode 100644 index 00000000..6bdc32ca --- /dev/null +++ b/src/platform/web/ui/session/CreateRoomView.js @@ -0,0 +1,105 @@ +/* +Copyright 2020 Bruno Windels +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +import {TemplateView} from "../general/TemplateView"; +import {AvatarView} from "../AvatarView"; +import {StaticView} from "../general/StaticView"; + +export class CreateRoomView extends TemplateView { + render(t, vm) { + return t.main({className: "CreateRoomView middle"}, [ + t.h2("Create room"), + //t.div({className: "RoomView_error"}, vm => vm.error), + t.form({className: "CreateRoomView_detailsForm form", onChange: evt => this.onFormChange(evt), onSubmit: evt => this.onSubmit(evt)}, [ + t.div({className: "vertical-layout"}, [ + t.button({type: "button", className: "CreateRoomView_selectAvatar", onClick: () => vm.selectAvatar()}, + t.mapView(vm => vm.hasAvatar, hasAvatar => { + if (hasAvatar) { + return new AvatarView(vm, 64); + } else { + return new StaticView(undefined, t => { + return t.div({className: "CreateRoomView_selectAvatarPlaceholder"}) + }); + } + }) + ), + t.div({className: "stretch form-row text"}, [ + t.label({for: "name"}, vm.i18n`Room name`), + t.input({ + onInput: evt => vm.setName(evt.target.value), + type: "text", name: "name", id: "name", + placeholder: vm.i18n`Enter a room name` + }, vm => vm.name), + ]), + ]), + t.div({className: "form-row text"}, [ + t.label({for: "topic"}, vm.i18n`Topic (optional)`), + t.textarea({ + onInput: evt => vm.setTopic(evt.target.value), + name: "topic", id: "topic", + placeholder: vm.i18n`Topic` + }), + ]), + t.div({className: "form-group"}, [ + t.div({className: "form-row check"}, [ + t.input({type: "radio", name: "isPublic", id: "isPrivate", value: "false", checked: !vm.isPublic}), + t.label({for: "isPrivate"}, vm.i18n`Private room, only upon invitation.`) + ]), + t.div({className: "form-row check"}, [ + t.input({type: "radio", name: "isPublic", id: "isPublic", value: "true", checked: vm.isPublic}), + t.label({for: "isPublic"}, vm.i18n`Public room, anyone can join`) + ]), + ]), + t.div({className: {"form-row check": true, hidden: vm => vm.isPublic}}, [ + t.input({type: "checkbox", name: "isEncrypted", id: "isEncrypted", checked: vm.isEncrypted}), + t.label({for: "isEncrypted"}, vm.i18n`Enable end-to-end encryption`) + ]), + t.div({className: {"form-row text": true, hidden: vm => !vm.isPublic}}, [ + t.label({for: "roomAlias"}, vm.i18n`Room alias`), + t.input({ + onInput: evt => vm.setRoomAlias(evt.target.value), + type: "text", name: "roomAlias", id: "roomAlias", + placeholder: vm.i18n`Room alias + `}), + ]), + t.div({className: "button-row"}, [ + t.button({ + className: "button-action primary", + type: "submit", + disabled: vm => !vm.canCreate + }, vm.i18n`Create room`), + ]), + ]) + ]); + } + + onFormChange(evt) { + switch (evt.target.name) { + case "isEncrypted": + this.value.setEncrypted(evt.target.checked); + break; + case "isPublic": + this.value.setPublic(evt.currentTarget.isPublic.value === "true"); + break; + } + } + + onSubmit(evt) { + evt.preventDefault(); + this.value.create(); + } +} From 74f7879cb6f19f4945508a1021643d5499d481d6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 09:39:27 +0100 Subject: [PATCH 131/160] fix unrelated bug: invite sorting order wasn't stable in left panel as the timestamp is the same when you receive the invite during your first sync --- src/domain/session/leftpanel/InviteTileViewModel.js | 6 +++++- src/matrix/room/create.ts | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/domain/session/leftpanel/InviteTileViewModel.js b/src/domain/session/leftpanel/InviteTileViewModel.js index 09c11fb8..bdc0e193 100644 --- a/src/domain/session/leftpanel/InviteTileViewModel.js +++ b/src/domain/session/leftpanel/InviteTileViewModel.js @@ -39,6 +39,10 @@ export class InviteTileViewModel extends BaseTileViewModel { if (parentComparison !== 0) { return parentComparison; } - return other._invite.timestamp - this._invite.timestamp; + const timeDiff = other._invite.timestamp - this._invite.timestamp; + if (timeDiff !== 0) { + return timeDiff; + } + return this._invite.id < other._invite.id ? -1 : 1; } } diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index e3a1ed4d..6dd9340a 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -194,7 +194,8 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { get name() { return this._calculatedName; } get isBeingCreated(): boolean { return true; } get error(): Error | undefined { return this._error; } - + get id() { return this.localId; } + cancel() { // TODO: remove from collection somehow } From 5f6308e7c440496d6466bc1a6a232a734a8e8c6c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 09:40:19 +0100 Subject: [PATCH 132/160] fix homeserver field style in login view --- src/platform/web/ui/css/form.css | 2 +- src/platform/web/ui/css/themes/element/theme.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/css/form.css b/src/platform/web/ui/css/form.css index f5452c65..97859f29 100644 --- a/src/platform/web/ui/css/form.css +++ b/src/platform/web/ui/css/form.css @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.form .form-row.text > input, .form .form-row.text > textarea { +.form-row.text > input, .form-row.text > textarea { display: block; width: 100%; min-width: 0; diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 3a0d34ba..a6040e7e 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -80,7 +80,7 @@ limitations under the License. flex: 1 0 auto; } -.form textarea { +.form-row.text textarea { font-family: "Inter", sans-serif; } From fed42f13ad7d95d9f0695ac8431509cce9c9b545 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 09:40:30 +0100 Subject: [PATCH 133/160] textarea styling --- src/platform/web/ui/css/themes/element/theme.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index a6040e7e..449de0c6 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -98,6 +98,7 @@ limitations under the License. border-radius: 8px; margin-top: 5px; font-size: 1em; + resize: vertical; } .form-row.check { From bbb1683dbfe06647f4a89ab091900c2ebb26bf02 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 09:40:42 +0100 Subject: [PATCH 134/160] fixup: login view styling --- src/platform/web/ui/login/LoginView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index e8e1a0bf..88002625 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -34,7 +34,7 @@ export class LoginView extends TemplateView { t.div({className: "logo"}), t.h1([vm.i18n`Sign In`]), t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null), - t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form form-row" }, + t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form-row text" }, [ t.label({for: "homeserver"}, vm.i18n`Homeserver`), t.input({ From d6d1af13d05a6bbf0715e805d1db0550acc743e5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 11:03:52 +0100 Subject: [PATCH 135/160] rename RoomBeingCreated.localId to id --- src/domain/session/CreateRoomViewModel.js | 2 +- .../RoomBeingCreatedTileViewModel.js | 2 +- .../rightpanel/MemberDetailsViewModel.js | 2 +- .../session/room/RoomBeingCreatedViewModel.js | 2 +- src/matrix/Session.js | 20 ++++++++++++------- src/matrix/room/create.ts | 6 ++---- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/domain/session/CreateRoomViewModel.js b/src/domain/session/CreateRoomViewModel.js index 47656128..b398b615 100644 --- a/src/domain/session/CreateRoomViewModel.js +++ b/src/domain/session/CreateRoomViewModel.js @@ -82,7 +82,7 @@ export class CreateRoomViewModel extends ViewModel { avatar, invites: ["@bwindels:matrix.org"] }); - this.navigation.push("room", roomBeingCreated.localId); + this.navigation.push("room", roomBeingCreated.id); } diff --git a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js index d7840bfa..c80fba6c 100644 --- a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js +++ b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js @@ -22,7 +22,7 @@ export class RoomBeingCreatedTileViewModel extends BaseTileViewModel { super(options); const {roomBeingCreated} = options; this._roomBeingCreated = roomBeingCreated; - this._url = this.urlCreator.openRoomActionUrl(this._roomBeingCreated.localId); + this._url = this.urlCreator.openRoomActionUrl(this._roomBeingCreated.id); } get busy() { return true; } diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index 68045eba..f5169afa 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -90,7 +90,7 @@ export class MemberDetailsViewModel extends ViewModel { type: RoomType.DirectMessage, invites: [this.userId] }); - roomId = roomBeingCreated.localId; + roomId = roomBeingCreated.id; } this.navigation.push("room", roomId); } diff --git a/src/domain/session/room/RoomBeingCreatedViewModel.js b/src/domain/session/room/RoomBeingCreatedViewModel.js index eebbb668..e9cb3616 100644 --- a/src/domain/session/room/RoomBeingCreatedViewModel.js +++ b/src/domain/session/room/RoomBeingCreatedViewModel.js @@ -32,7 +32,7 @@ export class RoomBeingCreatedViewModel extends ViewModel { get kind() { return "roomBeingCreated"; } get closeUrl() { return this._closeUrl; } get name() { return this._roomBeingCreated.name; } - get id() { return this._roomBeingCreated.localId; } + get id() { return this._roomBeingCreated.id; } get isEncrypted() { return this._roomBeingCreated.isEncrypted; } get avatarLetter() { diff --git a/src/matrix/Session.js b/src/matrix/Session.js index b03306ed..2d4516ea 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -64,7 +64,13 @@ export class Session { this._activeArchivedRooms = new Map(); this._invites = new ObservableMap(); this._inviteUpdateCallback = (invite, params) => this._invites.update(invite.id, params); - this._roomsBeingCreatedUpdateCallback = (rbc, params) => this._roomsBeingCreated.update(rbc.localId, params); + this._roomsBeingCreatedUpdateCallback = (rbc, params) => { + if (rbc.isCancelled) { + this._roomsBeingCreated.remove(rbc.id); + } else { + this._roomsBeingCreated.update(rbc.id, params) + } + }; this._roomsBeingCreated = new ObservableMap(); this._user = new User(sessionInfo.userId); this._deviceMessageHandler = new DeviceMessageHandler({storage}); @@ -603,11 +609,11 @@ export class Session { createRoom(options, log = undefined) { let roomBeingCreated; this._platform.logger.runDetached("create room", async log => { - const localId = `local-${Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)}`; + const id = `local-${Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)}`; roomBeingCreated = new RoomBeingCreated( - localId, options, this._roomsBeingCreatedUpdateCallback, + id, options, this._roomsBeingCreatedUpdateCallback, this._mediaRepository, this._platform, log); - this._roomsBeingCreated.set(localId, roomBeingCreated); + this._roomsBeingCreated.set(id, roomBeingCreated); const promises = [roomBeingCreated.create(this._hsApi, log)]; const loadProfiles = !(options.loadProfiles === false); // default to true if (loadProfiles) { @@ -709,15 +715,15 @@ export class Session { _tryReplaceRoomBeingCreated(roomId, log) { for (const [,roomBeingCreated] of this._roomsBeingCreated) { if (roomBeingCreated.roomId === roomId) { - const observableStatus = this._observedRoomStatus.get(roomBeingCreated.localId); + const observableStatus = this._observedRoomStatus.get(roomBeingCreated.id); if (observableStatus) { log.log(`replacing room being created`) - .set("localId", roomBeingCreated.localId) + .set("localId", roomBeingCreated.id) .set("roomId", roomBeingCreated.roomId); observableStatus.set(observableStatus.get() | RoomStatus.Replaced); } roomBeingCreated.dispose(); - this._roomsBeingCreated.remove(roomBeingCreated.localId); + this._roomsBeingCreated.remove(roomBeingCreated.id); return; } } diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index 6dd9340a..cc65892e 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -94,7 +94,7 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { private _error?: Error; constructor( - public readonly localId: string, + public readonly id: string, private readonly options: Options, private readonly updateCallback: (self: RoomBeingCreated, params: string | undefined) => void, public readonly mediaRepository: MediaRepository, @@ -187,15 +187,13 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { this.emit("change"); } - get avatarColorId(): string { return this.options.invites?.[0] ?? this._roomId ?? this.localId; } get avatarUrl(): string | undefined { return this.profiles?.[0].avatarUrl; } + get avatarColorId(): string { return this.options.invites?.[0] ?? this._roomId ?? this.id; } get avatarBlobUrl(): string | undefined { return this.options.avatar?.blob?.url; } get roomId(): string | undefined { return this._roomId; } get name() { return this._calculatedName; } get isBeingCreated(): boolean { return true; } get error(): Error | undefined { return this._error; } - get id() { return this.localId; } - cancel() { // TODO: remove from collection somehow } From f12841b2d37cad9c306d7e71820a2e60fe7905f8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 11:06:20 +0100 Subject: [PATCH 136/160] better error handling in RoomBeingCreated --- src/matrix/room/create.ts | 96 ++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index cc65892e..1127c7ac 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -117,44 +117,44 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { /** @internal */ async create(hsApi: HomeServerApi, log: ILogItem): Promise { - let avatarEventContent; - if (this.options.avatar) { - const {avatar} = this.options; - const attachment = new AttachmentUpload({filename: avatar.name, blob: avatar.blob, platform: this.platform}); - await attachment.upload(hsApi, () => {}, log); - avatarEventContent = { - info: avatar.info - }; - attachment.applyToContent("url", avatarEventContent); - } - const createOptions: CreateRoomPayload = { - is_direct: this.options.type === RoomType.DirectMessage, - preset: presetForType(this.options.type), - initial_state: [] - }; - if (this.options.name) { - createOptions.name = this.options.name; - } - if (this.options.topic) { - createOptions.topic = this.options.topic; - } - if (this.options.invites) { - createOptions.invite = this.options.invites; - } - if (this.options.alias) { - createOptions.room_alias_name = this.options.alias; - } - if (this.isEncrypted) { - createOptions.initial_state.push(createRoomEncryptionEvent()); - } - if (avatarEventContent) { - createOptions.initial_state.push({ - type: "m.room.avatar", - state_key: "", - content: avatarEventContent - }); - } try { + let avatarEventContent; + if (this.options.avatar) { + const {avatar} = this.options; + const attachment = new AttachmentUpload({filename: avatar.name, blob: avatar.blob, platform: this.platform}); + await attachment.upload(hsApi, () => {}, log); + avatarEventContent = { + info: avatar.info + }; + attachment.applyToContent("url", avatarEventContent); + } + const createOptions: CreateRoomPayload = { + is_direct: this.options.type === RoomType.DirectMessage, + preset: presetForType(this.options.type), + initial_state: [] + }; + if (this.options.name) { + createOptions.name = this.options.name; + } + if (this.options.topic) { + createOptions.topic = this.options.topic; + } + if (this.options.invites) { + createOptions.invite = this.options.invites; + } + if (this.options.alias) { + createOptions.room_alias_name = this.options.alias; + } + if (this.isEncrypted) { + createOptions.initial_state.push(createRoomEncryptionEvent()); + } + if (avatarEventContent) { + createOptions.initial_state.push({ + type: "m.room.avatar", + state_key: "", + content: avatarEventContent + }); + } const response = await hsApi.createRoom(createOptions, {log}).response(); this._roomId = response["room_id"]; } catch (err) { @@ -170,16 +170,18 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { * is running. */ /** @internal */ async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise { - // only load profiles if we need it for the room name and avatar - if (!this.options.name && this.options.invites) { - this.profiles = await loadProfiles(this.options.invites, hsApi, log); - const summaryData = { - joinCount: 1, // ourselves - inviteCount: this.options.invites.length - }; - this._calculatedName = calculateRoomName(this.profiles, summaryData, log); - this.emitChange(); - } + try { + // only load profiles if we need it for the room name and avatar + if (!this.options.name && this.options.invites) { + this.profiles = await loadProfiles(this.options.invites, hsApi, log); + const summaryData = { + joinCount: 1, // ourselves + inviteCount: this.options.invites.length + }; + this._calculatedName = calculateRoomName(this.profiles, summaryData, log); + this.emitChange(); + } + } catch (err) {} // swallow error, loading profiles is not essential } private emitChange(params?: string) { From e8c20c28b28825ba877ed6ab8d571ab8acdb7975 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 11:06:44 +0100 Subject: [PATCH 137/160] allow passing label into LoadingView also doesn't need to be a template view, as it doesn't have bindings or event handlers --- src/platform/web/ui/general/LoadingView.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ui/general/LoadingView.js b/src/platform/web/ui/general/LoadingView.js index f2ab20cc..436d189c 100644 --- a/src/platform/web/ui/general/LoadingView.js +++ b/src/platform/web/ui/general/LoadingView.js @@ -14,11 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {TemplateView} from "./TemplateView"; +import {StaticView} from "./StaticView"; import {spinner} from "../common.js"; -export class LoadingView extends TemplateView { - render(t) { - return t.div({ className: "LoadingView" }, [spinner(t), "Loading"]); +export class LoadingView extends StaticView { + constructor(label = "Loading") { + super(label, (t, label) => { + return t.div({ className: "LoadingView" }, [spinner(t), label]); + }); } } From 20493f9e87a7dadd9b7105e6e73d82645faaa0a2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 11:07:13 +0100 Subject: [PATCH 138/160] cleanup --- src/matrix/Session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 2d4516ea..a5f48bf3 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -615,7 +615,7 @@ export class Session { this._mediaRepository, this._platform, log); this._roomsBeingCreated.set(id, roomBeingCreated); const promises = [roomBeingCreated.create(this._hsApi, log)]; - const loadProfiles = !(options.loadProfiles === false); // default to true + const loadProfiles = options.loadProfiles !== false; // default to true if (loadProfiles) { promises.push(roomBeingCreated.loadProfiles(this._hsApi, log)); } From b5536830d008a288998f3bf00590a57fe1e4b3fd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 11:07:29 +0100 Subject: [PATCH 139/160] improve RoomBeingCreatedView, allow removing the roombeingcreated --- .../session/room/RoomBeingCreatedViewModel.js | 26 ++-- src/matrix/room/create.ts | 17 ++- .../web/ui/css/themes/element/theme.css | 13 +- src/platform/web/ui/session/CreateRoomView.js | 122 +++++++++--------- .../ui/session/room/RoomBeingCreatedView.js | 35 ++++- 5 files changed, 132 insertions(+), 81 deletions(-) diff --git a/src/domain/session/room/RoomBeingCreatedViewModel.js b/src/domain/session/room/RoomBeingCreatedViewModel.js index e9cb3616..f15838df 100644 --- a/src/domain/session/room/RoomBeingCreatedViewModel.js +++ b/src/domain/session/room/RoomBeingCreatedViewModel.js @@ -34,21 +34,15 @@ export class RoomBeingCreatedViewModel extends ViewModel { get name() { return this._roomBeingCreated.name; } get id() { return this._roomBeingCreated.id; } get isEncrypted() { return this._roomBeingCreated.isEncrypted; } - - get avatarLetter() { - return avatarInitials(this.name); - } - - get avatarColorNumber() { - return getIdentifierColorNumber(this._roomBeingCreated.avatarColorId); - } + get error() { return this._roomBeingCreated.error?.message; } + get avatarLetter() { return avatarInitials(this.name); } + get avatarColorNumber() { return getIdentifierColorNumber(this._roomBeingCreated.avatarColorId); } + get avatarTitle() { return this.name; } avatarUrl(size) { - return getAvatarHttpUrl(this._roomBeingCreated.avatarUrl, size, this.platform, this._mediaRepository); - } - - get avatarTitle() { - return this.name; + // allow blob url which doesn't need mxc => http resolution + return this._roomBeingCreated.avatarBlobUrl ?? + getAvatarHttpUrl(this._roomBeingCreated.avatarUrl, size, this.platform, this._mediaRepository); } focus() {} @@ -57,6 +51,12 @@ export class RoomBeingCreatedViewModel extends ViewModel { this.emitChange(); } + cancel() { + this._roomBeingCreated.cancel(); + // navigate away from the room + this.navigation.applyPath(this.navigation.path.until("session")); + } + dispose() { super.dispose(); this._roomBeingCreated.off("change", this._onRoomChange); diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index 1127c7ac..6a1fc947 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -92,6 +92,7 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { public readonly isEncrypted: boolean; private _calculatedName: string; private _error?: Error; + private _isCancelled = false; constructor( public readonly id: string, @@ -189,17 +190,25 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { this.emit("change"); } - get avatarUrl(): string | undefined { return this.profiles?.[0].avatarUrl; } get avatarColorId(): string { return this.options.invites?.[0] ?? this._roomId ?? this.id; } + get avatarUrl(): string | undefined { return this.profiles?.[0]?.avatarUrl; } get avatarBlobUrl(): string | undefined { return this.options.avatar?.blob?.url; } get roomId(): string | undefined { return this._roomId; } get name() { return this._calculatedName; } get isBeingCreated(): boolean { return true; } get error(): Error | undefined { return this._error; } - cancel() { - // TODO: remove from collection somehow - } + cancel() { + if (!this._isCancelled) { + this.dispose(); + this._isCancelled = true; + this.emitChange("isCancelled"); + } + } + // called from Session when updateCallback is invoked to remove it from the collection + get isCancelled() { return this._isCancelled; } + + /** @internal */ dispose() { if (this.options.avatar) { this.options.avatar.blob.dispose(); diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 449de0c6..87d82485 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1095,10 +1095,17 @@ button.RoomDetailsView_row::after { gap: 12px; } -.CreateRoomView { - padding: 0 12px; - justify-self: center; +.CreateRoomView, .RoomBeingCreated_error { max-width: 400px; +} + +.RoomBeingCreated_error { + margin-top: 48px; +} + +.centered-column { + padding: 0 12px; + align-self: center; width: 100%; box-sizing: border-box; } diff --git a/src/platform/web/ui/session/CreateRoomView.js b/src/platform/web/ui/session/CreateRoomView.js index 6bdc32ca..c447710f 100644 --- a/src/platform/web/ui/session/CreateRoomView.js +++ b/src/platform/web/ui/session/CreateRoomView.js @@ -21,70 +21,72 @@ import {StaticView} from "../general/StaticView"; export class CreateRoomView extends TemplateView { render(t, vm) { - return t.main({className: "CreateRoomView middle"}, [ - t.h2("Create room"), - //t.div({className: "RoomView_error"}, vm => vm.error), - t.form({className: "CreateRoomView_detailsForm form", onChange: evt => this.onFormChange(evt), onSubmit: evt => this.onSubmit(evt)}, [ - t.div({className: "vertical-layout"}, [ - t.button({type: "button", className: "CreateRoomView_selectAvatar", onClick: () => vm.selectAvatar()}, - t.mapView(vm => vm.hasAvatar, hasAvatar => { - if (hasAvatar) { - return new AvatarView(vm, 64); - } else { - return new StaticView(undefined, t => { - return t.div({className: "CreateRoomView_selectAvatarPlaceholder"}) - }); - } - }) - ), - t.div({className: "stretch form-row text"}, [ - t.label({for: "name"}, vm.i18n`Room name`), + return t.main({className: "middle"}, + t.div({className: "CreateRoomView centered-column"}, [ + t.h2("Create room"), + //t.div({className: "RoomView_error"}, vm => vm.error), + t.form({className: "CreateRoomView_detailsForm form", onChange: evt => this.onFormChange(evt), onSubmit: evt => this.onSubmit(evt)}, [ + t.div({className: "vertical-layout"}, [ + t.button({type: "button", className: "CreateRoomView_selectAvatar", onClick: () => vm.selectAvatar()}, + t.mapView(vm => vm.hasAvatar, hasAvatar => { + if (hasAvatar) { + return new AvatarView(vm, 64); + } else { + return new StaticView(undefined, t => { + return t.div({className: "CreateRoomView_selectAvatarPlaceholder"}) + }); + } + }) + ), + t.div({className: "stretch form-row text"}, [ + t.label({for: "name"}, vm.i18n`Room name`), + t.input({ + onInput: evt => vm.setName(evt.target.value), + type: "text", name: "name", id: "name", + placeholder: vm.i18n`Enter a room name` + }, vm => vm.name), + ]), + ]), + t.div({className: "form-row text"}, [ + t.label({for: "topic"}, vm.i18n`Topic (optional)`), + t.textarea({ + onInput: evt => vm.setTopic(evt.target.value), + name: "topic", id: "topic", + placeholder: vm.i18n`Topic` + }), + ]), + t.div({className: "form-group"}, [ + t.div({className: "form-row check"}, [ + t.input({type: "radio", name: "isPublic", id: "isPrivate", value: "false", checked: !vm.isPublic}), + t.label({for: "isPrivate"}, vm.i18n`Private room, only upon invitation.`) + ]), + t.div({className: "form-row check"}, [ + t.input({type: "radio", name: "isPublic", id: "isPublic", value: "true", checked: vm.isPublic}), + t.label({for: "isPublic"}, vm.i18n`Public room, anyone can join`) + ]), + ]), + t.div({className: {"form-row check": true, hidden: vm => vm.isPublic}}, [ + t.input({type: "checkbox", name: "isEncrypted", id: "isEncrypted", checked: vm.isEncrypted}), + t.label({for: "isEncrypted"}, vm.i18n`Enable end-to-end encryption`) + ]), + t.div({className: {"form-row text": true, hidden: vm => !vm.isPublic}}, [ + t.label({for: "roomAlias"}, vm.i18n`Room alias`), t.input({ - onInput: evt => vm.setName(evt.target.value), - type: "text", name: "name", id: "name", - placeholder: vm.i18n`Enter a room name` - }, vm => vm.name), + onInput: evt => vm.setRoomAlias(evt.target.value), + type: "text", name: "roomAlias", id: "roomAlias", + placeholder: vm.i18n`Room alias + `}), ]), - ]), - t.div({className: "form-row text"}, [ - t.label({for: "topic"}, vm.i18n`Topic (optional)`), - t.textarea({ - onInput: evt => vm.setTopic(evt.target.value), - name: "topic", id: "topic", - placeholder: vm.i18n`Topic` - }), - ]), - t.div({className: "form-group"}, [ - t.div({className: "form-row check"}, [ - t.input({type: "radio", name: "isPublic", id: "isPrivate", value: "false", checked: !vm.isPublic}), - t.label({for: "isPrivate"}, vm.i18n`Private room, only upon invitation.`) + t.div({className: "button-row"}, [ + t.button({ + className: "button-action primary", + type: "submit", + disabled: vm => !vm.canCreate + }, vm.i18n`Create room`), ]), - t.div({className: "form-row check"}, [ - t.input({type: "radio", name: "isPublic", id: "isPublic", value: "true", checked: vm.isPublic}), - t.label({for: "isPublic"}, vm.i18n`Public room, anyone can join`) - ]), - ]), - t.div({className: {"form-row check": true, hidden: vm => vm.isPublic}}, [ - t.input({type: "checkbox", name: "isEncrypted", id: "isEncrypted", checked: vm.isEncrypted}), - t.label({for: "isEncrypted"}, vm.i18n`Enable end-to-end encryption`) - ]), - t.div({className: {"form-row text": true, hidden: vm => !vm.isPublic}}, [ - t.label({for: "roomAlias"}, vm.i18n`Room alias`), - t.input({ - onInput: evt => vm.setRoomAlias(evt.target.value), - type: "text", name: "roomAlias", id: "roomAlias", - placeholder: vm.i18n`Room alias - `}), - ]), - t.div({className: "button-row"}, [ - t.button({ - className: "button-action primary", - type: "submit", - disabled: vm => !vm.canCreate - }, vm.i18n`Create room`), - ]), + ]) ]) - ]); + ); } onFormChange(evt) { diff --git a/src/platform/web/ui/session/room/RoomBeingCreatedView.js b/src/platform/web/ui/session/room/RoomBeingCreatedView.js index df26a43d..1da44d34 100644 --- a/src/platform/web/ui/session/room/RoomBeingCreatedView.js +++ b/src/platform/web/ui/session/room/RoomBeingCreatedView.js @@ -16,10 +16,43 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView"; +import {LoadingView} from "../../general/LoadingView"; +import {AvatarView} from "../../AvatarView"; import {renderStaticAvatar} from "../../avatar.js"; export class RoomBeingCreatedView extends TemplateView { render(t, vm) { - return t.h1({className: "middle"}, ["creating room", vm => vm.name]); + return t.main({className: "RoomView middle"}, [ + t.div({className: "RoomHeader middle-header"}, [ + t.a({className: "button-utility close-middle", href: vm.closeUrl, title: vm.i18n`Close room`}), + t.view(new AvatarView(vm, 32)), + t.div({className: "room-description"}, [ + t.h2(vm => vm.name), + ]) + ]), + t.div({className: "RoomView_body"}, [ + t.mapView(vm => vm.error, error => { + if (error) { + return new ErrorView(vm); + } else { + return new LoadingView(vm.i18n`Setting up the room…`); + } + }) + ]) + ]); + } +} + +class ErrorView extends TemplateView { + render(t,vm) { + return t.div({className: "RoomBeingCreated_error centered-column"}, [ + t.h3(vm.i18n`Could not create the room, something went wrong:`), + t.div({className: "RoomView_error form-group"}, vm.error), + t.div({className: "button-row"}, + t.button({ + className: "button-action primary destructive", + onClick: () => vm.cancel() + }, vm.i18n`Cancel`)) + ]); } } From 024a6c06aa364dfb817ede8805adc05b4c53af00 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 11:11:15 +0100 Subject: [PATCH 140/160] handle offline error nicer --- src/domain/session/room/RoomBeingCreatedViewModel.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/domain/session/room/RoomBeingCreatedViewModel.js b/src/domain/session/room/RoomBeingCreatedViewModel.js index f15838df..0ed82be9 100644 --- a/src/domain/session/room/RoomBeingCreatedViewModel.js +++ b/src/domain/session/room/RoomBeingCreatedViewModel.js @@ -34,7 +34,16 @@ export class RoomBeingCreatedViewModel extends ViewModel { get name() { return this._roomBeingCreated.name; } get id() { return this._roomBeingCreated.id; } get isEncrypted() { return this._roomBeingCreated.isEncrypted; } - get error() { return this._roomBeingCreated.error?.message; } + get error() { + const {error} = this._roomBeingCreated; + if (error) { + if (error.name === "ConnectionError") { + return this.i18n`You seem to be offline`; + } else { + return error.message; + } + } + } get avatarLetter() { return avatarInitials(this.name); } get avatarColorNumber() { return getIdentifierColorNumber(this._roomBeingCreated.avatarColorId); } get avatarTitle() { return this.name; } From 4c0167ed74cfc00f67ca8db6409d19470a396ffd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 11:19:43 +0100 Subject: [PATCH 141/160] don't show spinner in left panel when room creation fails --- .../RoomBeingCreatedTileViewModel.js | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js index c80fba6c..e6fc4cee 100644 --- a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js +++ b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js @@ -25,15 +25,13 @@ export class RoomBeingCreatedTileViewModel extends BaseTileViewModel { this._url = this.urlCreator.openRoomActionUrl(this._roomBeingCreated.id); } - get busy() { return true; } - - get kind() { - return "roomBeingCreated"; - } - - get url() { - return this._url; - } + get busy() { return !this._roomBeingCreated.error; } + get kind() { return "roomBeingCreated"; } + get isHighlighted() { return !this.busy; } + get badgeCount() { return !this.busy && this.i18n`Failed`; } + get url() { return this._url; } + get name() { return this._roomBeingCreated.name; } + get _avatarSource() { return this._roomBeingCreated; } compare(other) { const parentComparison = super.compare(other); @@ -43,14 +41,6 @@ export class RoomBeingCreatedTileViewModel extends BaseTileViewModel { return other._roomBeingCreated.name.localeCompare(this._roomBeingCreated.name); } - get name() { - return this._roomBeingCreated.name; - } - - get _avatarSource() { - return this._roomBeingCreated; - } - avatarUrl(size) { // allow blob url which doesn't need mxc => http resolution return this._roomBeingCreated.avatarBlobUrl ?? super.avatarUrl(size); From 147810864f530a113a7a37b82d935c7f239eaf13 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 14:09:18 +0100 Subject: [PATCH 142/160] add support to set alias and federation flag in create room --- src/domain/session/CreateRoomViewModel.js | 64 ++++++++++++------- src/matrix/room/create.ts | 7 ++ .../web/ui/css/themes/element/theme.css | 6 ++ src/platform/web/ui/session/CreateRoomView.js | 19 +++++- 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/src/domain/session/CreateRoomViewModel.js b/src/domain/session/CreateRoomViewModel.js index b398b615..6e207e31 100644 --- a/src/domain/session/CreateRoomViewModel.js +++ b/src/domain/session/CreateRoomViewModel.js @@ -23,45 +23,60 @@ export class CreateRoomViewModel extends ViewModel { super(options); const {session} = options; this._session = session; - this._name = ""; - this._topic = ""; + this._name = undefined; + this._topic = undefined; + this._roomAlias = undefined; this._isPublic = false; this._isEncrypted = true; + this._isAdvancedShown = false; + this._isFederationDisabled = false; this._avatarScaledBlob = undefined; this._avatarFileName = undefined; this._avatarInfo = undefined; } + get isPublic() { return this._isPublic; } + get isEncrypted() { return this._isEncrypted; } + get canCreate() { return !!this._name; } + avatarUrl() { return this._avatarScaledBlob.url; } + get avatarTitle() { return this._name; } + get avatarLetter() { return ""; } + get avatarColorNumber() { return 0; } + get hasAvatar() { return !!this._avatarScaledBlob; } + get isFederationDisabled() { return this._isFederationDisabled; } + get isAdvancedShown() { return this._isAdvancedShown; } + setName(name) { this._name = name; - this.emitChange("name"); + this.emitChange("canCreate"); } - get name() { return this._name; } + setRoomAlias(roomAlias) { + this._roomAlias = roomAlias; + } setTopic(topic) { this._topic = topic; - this.emitChange("topic"); } - get topic() { return this._topic; } - setPublic(isPublic) { this._isPublic = isPublic; this.emitChange("isPublic"); } - get isPublic() { return this._isPublic; } - setEncrypted(isEncrypted) { this._isEncrypted = isEncrypted; this.emitChange("isEncrypted"); } - get isEncrypted() { return this._isEncrypted; } + setFederationDisabled(disable) { + this._isFederationDisabled = disable; + this.emitChange("isFederationDisabled"); + } - get canCreate() { - return !!this.name; + toggleAdvancedShown() { + this._isAdvancedShown = !this._isAdvancedShown; + this.emitChange("isAdvancedShown"); } create() { @@ -75,24 +90,16 @@ export class CreateRoomViewModel extends ViewModel { } const roomBeingCreated = this._session.createRoom({ type: this.isPublic ? RoomType.Public : RoomType.Private, - name: this.name ?? undefined, + name: this._name ?? undefined, topic: this.topic ?? undefined, isEncrypted: !this.isPublic && this._isEncrypted, - alias: this.isPublic ? this.roomAlias : undefined, + isFederationDisabled: this._isFederationDisabled, + alias: this.isPublic ? ensureAliasIsLocalPart(this._roomAlias) : undefined, avatar, - invites: ["@bwindels:matrix.org"] }); this.navigation.push("room", roomBeingCreated.id); } - - avatarUrl() { return this._avatarScaledBlob.url; } - get avatarTitle() { return this.name; } - get avatarLetter() { return ""; } - get avatarColorNumber() { return 0; } - get hasAvatar() { return !!this._avatarScaledBlob; } - get error() { return ""; } - async selectAvatar() { if (!this.platform.hasReadPixelPermission()) { alert("Please allow canvas image data access, so we can scale your images down."); @@ -124,3 +131,14 @@ export class CreateRoomViewModel extends ViewModel { this.emitChange("hasAvatar"); } } + +function ensureAliasIsLocalPart(roomAliasLocalPart) { + if (roomAliasLocalPart.startsWith("#")) { + roomAliasLocalPart = roomAliasLocalPart.substr(1); + } + const colonIdx = roomAliasLocalPart.indexOf(":"); + if (colonIdx !== -1) { + roomAliasLocalPart = roomAliasLocalPart.substr(0, colonIdx); + } + return roomAliasLocalPart; +} diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index 6a1fc947..c50aba6b 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -32,6 +32,7 @@ type CreateRoomPayload = { topic?: string; invite?: string[]; room_alias_name?: string; + creation_content?: {"m.federate": boolean}; initial_state: {type: string; state_key: string; content: Record}[] } @@ -51,6 +52,7 @@ type Avatar = { type Options = { type: RoomType; isEncrypted?: boolean; + isFederationDisabled?: boolean; name?: string; topic?: string; invites?: string[]; @@ -146,6 +148,11 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { if (this.options.alias) { createOptions.room_alias_name = this.options.alias; } + if (this.options.isFederationDisabled === true) { + createOptions.creation_content = { + "m.federate": false + }; + } if (this.isEncrypted) { createOptions.initial_state.push(createRoomEncryptionEvent()); } diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 87d82485..a3fb12ee 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -111,6 +111,12 @@ limitations under the License. display: block; } +.form-row .form-row-description { + font-size: 1rem; + color: #777; + margin: 8px 0 0 0; +} + .button-action { cursor: pointer; } diff --git a/src/platform/web/ui/session/CreateRoomView.js b/src/platform/web/ui/session/CreateRoomView.js index c447710f..9d6c6bbc 100644 --- a/src/platform/web/ui/session/CreateRoomView.js +++ b/src/platform/web/ui/session/CreateRoomView.js @@ -44,7 +44,7 @@ export class CreateRoomView extends TemplateView { onInput: evt => vm.setName(evt.target.value), type: "text", name: "name", id: "name", placeholder: vm.i18n`Enter a room name` - }, vm => vm.name), + }), ]), ]), t.div({className: "form-row text"}, [ @@ -74,8 +74,18 @@ export class CreateRoomView extends TemplateView { t.input({ onInput: evt => vm.setRoomAlias(evt.target.value), type: "text", name: "roomAlias", id: "roomAlias", - placeholder: vm.i18n`Room alias - `}), + placeholder: vm.i18n`Room alias (, or # or #:hs.tld`}), + ]), + t.div({className: "form-group"}, [ + t.div(t.button({className: "link", type: "button", onClick: () => vm.toggleAdvancedShown()}, + vm => vm.isAdvancedShown ? vm.i18n`Hide advanced settings` : vm.i18n`Show advanced settings`)), + t.div({className: {"form-row check": true, hidden: vm => !vm.isAdvancedShown}}, [ + t.input({type: "checkbox", name: "isFederationDisabled", id: "isFederationDisabled", checked: vm.isFederationDisabled}), + t.label({for: "isFederationDisabled"}, [ + vm.i18n`Disable federation`, + t.p({className: "form-row-description"}, vm.i18n`Can't be changed later. This will prevent people on other homeservers from joining the room. This is typically used when only people from your own organisation (if applicable) should be allowed in the room, and is otherwise not needed.`) + ]), + ]), ]), t.div({className: "button-row"}, [ t.button({ @@ -97,6 +107,9 @@ export class CreateRoomView extends TemplateView { case "isPublic": this.value.setPublic(evt.currentTarget.isPublic.value === "true"); break; + case "isFederationDisabled": + this.value.setFederationDisabled(evt.target.checked); + break; } } From 955a6bd6f928468edf1ffec431203183c84816ca Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 14:38:12 +0100 Subject: [PATCH 143/160] styling for button in member details to open DM --- src/platform/web/ui/css/themes/element/theme.css | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index a3fb12ee..383feeae 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1074,10 +1074,14 @@ button.RoomDetailsView_row::after { flex-direction: column; } -.MemberDetailsView_options a{ +.MemberDetailsView_options a, .MemberDetailsView_options button { color: #0dbd8b; text-decoration: none; - margin-bottom: 3px; + margin: 0 0 3px 0; + padding: 0; + border: none; + background: none; + cursor: pointer; } .LazyListParent { From 75bbde598da083de04b136b5299bea2aa8529a96 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 14:39:18 +0100 Subject: [PATCH 144/160] also consider rooms without a name and just you and the other a DM as we don't process m.direct account data yet --- src/matrix/room/BaseRoom.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 446c22d1..dda3e2e5 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -421,7 +421,18 @@ export class BaseRoom extends EventEmitter { } isDirectMessageForUserId(userId) { - return this._summary.data.dmUserId === userId; + if (this._summary.data.dmUserId === userId) { + return true; + } else { + // fall back to considering any room a DM containing heroes (e.g. no name) and 2 members, + // on of which the userId we're looking for. + // We need this because we're not yet processing m.direct account data correctly. + const {heroes, joinCount, inviteCount} = this._summary.data; + if (heroes && heroes.includes(userId) && (joinCount + inviteCount) === 2) { + return true; + } + } + return false; } async _loadPowerLevels() { From 2c1b29e637bd6a387bf0965187670dd0ebffcfb6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 14:39:41 +0100 Subject: [PATCH 145/160] remove logging --- src/domain/session/RoomViewModelObservable.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/domain/session/RoomViewModelObservable.js b/src/domain/session/RoomViewModelObservable.js index 02a9a60b..cec2e221 100644 --- a/src/domain/session/RoomViewModelObservable.js +++ b/src/domain/session/RoomViewModelObservable.js @@ -60,13 +60,10 @@ export class RoomViewModelObservable extends ObservableValue { } async _statusToViewModel(status) { - console.log("RoomViewModelObservable received status", status); if (status & RoomStatus.Replaced) { - console.log("replaced!"); if (status & RoomStatus.BeingCreated) { const {session} = this._sessionViewModel._client; const roomBeingCreated = session.roomsBeingCreated.get(this.id); - console.log("new id is", roomBeingCreated.roomId); this._sessionViewModel.navigation.push("room", roomBeingCreated.roomId); } else { throw new Error("Don't know how to replace a room with this status: " + (status ^ RoomStatus.Replaced)); From b0d790543a03f432084c79fa287800bfda43303b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 14:57:48 +0100 Subject: [PATCH 146/160] push to navigation in SessionViewModel rather than RVO --- src/domain/session/RoomViewModelObservable.js | 2 +- src/domain/session/SessionViewModel.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/domain/session/RoomViewModelObservable.js b/src/domain/session/RoomViewModelObservable.js index cec2e221..8fd0daf3 100644 --- a/src/domain/session/RoomViewModelObservable.js +++ b/src/domain/session/RoomViewModelObservable.js @@ -64,7 +64,7 @@ export class RoomViewModelObservable extends ObservableValue { if (status & RoomStatus.BeingCreated) { const {session} = this._sessionViewModel._client; const roomBeingCreated = session.roomsBeingCreated.get(this.id); - this._sessionViewModel.navigation.push("room", roomBeingCreated.roomId); + this._sessionViewModel.notifyRoomReplaced(roomBeingCreated.id, roomBeingCreated.roomId); } else { throw new Error("Don't know how to replace a room with this status: " + (status ^ RoomStatus.Replaced)); } diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 5a818c4d..24276f42 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -298,4 +298,7 @@ export class SessionViewModel extends ViewModel { this.emitChange("rightPanelViewModel"); } + notifyRoomReplaced(oldId, newId) { + this.navigation.push("room", newId); + } } From 30c8ea29b2a4ab619641c697c494114a69bc34ea Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 16:27:32 +0100 Subject: [PATCH 147/160] fix bug where the wrong left panel tile is removed when accepting invite because when comparing a tile to itself it wasn't returned 0 --- .../session/leftpanel/BaseTileViewModel.js | 2 +- .../session/leftpanel/InviteTileViewModel.js | 20 +++++++++++-- .../RoomBeingCreatedTileViewModel.js | 30 ++++++++++++++++--- .../session/leftpanel/RoomTileViewModel.js | 3 ++ src/domain/session/leftpanel/common.js | 23 ++++++++++++++ 5 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 src/domain/session/leftpanel/common.js diff --git a/src/domain/session/leftpanel/BaseTileViewModel.js b/src/domain/session/leftpanel/BaseTileViewModel.js index 95e91458..b360b1d4 100644 --- a/src/domain/session/leftpanel/BaseTileViewModel.js +++ b/src/domain/session/leftpanel/BaseTileViewModel.js @@ -18,7 +18,7 @@ limitations under the License. import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; import {ViewModel} from "../../ViewModel.js"; -const KIND_ORDER = ["invite", "room"]; +const KIND_ORDER = ["roomBeingCreated", "invite", "room"]; export class BaseTileViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/leftpanel/InviteTileViewModel.js b/src/domain/session/leftpanel/InviteTileViewModel.js index bdc0e193..cdd955b1 100644 --- a/src/domain/session/leftpanel/InviteTileViewModel.js +++ b/src/domain/session/leftpanel/InviteTileViewModel.js @@ -1,5 +1,4 @@ /* -Copyright 2020 Bruno Windels Copyright 2020, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +15,7 @@ limitations under the License. */ import {BaseTileViewModel} from "./BaseTileViewModel.js"; +import {comparePrimitive} from "./common"; export class InviteTileViewModel extends BaseTileViewModel { constructor(options) { @@ -34,6 +34,9 @@ export class InviteTileViewModel extends BaseTileViewModel { get badgeCount() { return this.i18n`!`; } get _avatarSource() { return this._invite; } + /** very important that sorting order is stable and that comparing + * to itself always returns 0, otherwise SortedMapList will + * remove the wrong children, etc ... */ compare(other) { const parentComparison = super.compare(other); if (parentComparison !== 0) { @@ -43,6 +46,19 @@ export class InviteTileViewModel extends BaseTileViewModel { if (timeDiff !== 0) { return timeDiff; } - return this._invite.id < other._invite.id ? -1 : 1; + return comparePrimitive(this._invite.id, other._invite.id); + } +} + +export function tests() { + return { + "test compare with timestamp": assert => { + const urlCreator = {openRoomActionUrl() { return "";}} + const vm1 = new InviteTileViewModel({invite: {timestamp: 500, id: "1"}, urlCreator}); + const vm2 = new InviteTileViewModel({invite: {timestamp: 250, id: "2"}, urlCreator}); + assert(vm1.compare(vm2) < 0); + assert(vm2.compare(vm1) > 0); + assert.equal(vm1.compare(vm1), 0); + }, } } diff --git a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js index e6fc4cee..81785bd9 100644 --- a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js +++ b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js @@ -16,6 +16,7 @@ limitations under the License. */ import {BaseTileViewModel} from "./BaseTileViewModel.js"; +import {comparePrimitive} from "./common"; export class RoomBeingCreatedTileViewModel extends BaseTileViewModel { constructor(options) { @@ -33,12 +34,20 @@ export class RoomBeingCreatedTileViewModel extends BaseTileViewModel { get name() { return this._roomBeingCreated.name; } get _avatarSource() { return this._roomBeingCreated; } + /** very important that sorting order is stable and that comparing + * to itself always returns 0, otherwise SortedMapList will + * remove the wrong children, etc ... */ compare(other) { - const parentComparison = super.compare(other); - if (parentComparison !== 0) { - return parentComparison; + const parentCmp = super.compare(other); + if (parentCmp !== 0) { + return parentCmp; + } + const nameCmp = comparePrimitive(this.name, other.name); + if (nameCmp === 0) { + return comparePrimitive(this._roomBeingCreated.id, other._roomBeingCreated.id); + } else { + return nameCmp; } - return other._roomBeingCreated.name.localeCompare(this._roomBeingCreated.name); } avatarUrl(size) { @@ -46,3 +55,16 @@ export class RoomBeingCreatedTileViewModel extends BaseTileViewModel { return this._roomBeingCreated.avatarBlobUrl ?? super.avatarUrl(size); } } + +export function tests() { + return { + "test compare with names": assert => { + const urlCreator = {openRoomActionUrl() { return "";}} + const vm1 = new RoomBeingCreatedTileViewModel({roomBeingCreated: {name: "A", id: "1"}, urlCreator}); + const vm2 = new RoomBeingCreatedTileViewModel({roomBeingCreated: {name: "B", id: "2"}, urlCreator}); + assert(vm1.compare(vm2) < 0); + assert(vm2.compare(vm1) > 0); + assert.equal(vm1.compare(vm1), 0); + }, + } +} diff --git a/src/domain/session/leftpanel/RoomTileViewModel.js b/src/domain/session/leftpanel/RoomTileViewModel.js index eebea618..8c38bb3e 100644 --- a/src/domain/session/leftpanel/RoomTileViewModel.js +++ b/src/domain/session/leftpanel/RoomTileViewModel.js @@ -33,6 +33,9 @@ export class RoomTileViewModel extends BaseTileViewModel { return this._url; } + /** very important that sorting order is stable and that comparing + * to itself always returns 0, otherwise SortedMapList will + * remove the wrong children, etc ... */ compare(other) { const parentComparison = super.compare(other); if (parentComparison !== 0) { diff --git a/src/domain/session/leftpanel/common.js b/src/domain/session/leftpanel/common.js new file mode 100644 index 00000000..3af95eba --- /dev/null +++ b/src/domain/session/leftpanel/common.js @@ -0,0 +1,23 @@ +/* +Copyright 2020, 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +export function comparePrimitive(a, b) { + if (a === b) { + return 0; + } else { + return a < b ? -1 : 1; + } +} From 15eecbb46358a1fb7e625dba0485a6ad34ad0d74 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 16:28:44 +0100 Subject: [PATCH 148/160] cleanup --- src/domain/session/leftpanel/LeftPanelViewModel.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index cc3a2ccb..08584d7f 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -40,7 +40,7 @@ export class LeftPanelViewModel extends ViewModel { _mapTileViewModels(roomsBeingCreated, invites, rooms) { // join is not commutative, invites will take precedence over rooms - const joined = invites.join(roomsBeingCreated, rooms).mapValues((item, emitChange) => { + const allTiles = invites.join(roomsBeingCreated, rooms).mapValues((item, emitChange) => { let vm; if (item.isBeingCreated) { vm = new RoomBeingCreatedTileViewModel(this.childOptions({roomBeingCreated: item, emitChange})); @@ -56,10 +56,7 @@ export class LeftPanelViewModel extends ViewModel { } return vm; }); - return joined; - // return new LogMap(joined, (op, key, value) => { - // console.log("room list", op, key, value); - // }); + return allTiles; } _updateCurrentVM(vm) { From 8526461d3c28abab2b55177e5aa98012eaa0493d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 16:39:54 +0100 Subject: [PATCH 149/160] split up create code into separate files --- src/domain/session/CreateRoomViewModel.js | 2 +- src/domain/session/RoomViewModelObservable.js | 2 +- .../rightpanel/MemberDetailsViewModel.js | 2 +- src/matrix/Session.js | 4 +- src/matrix/common.js | 2 +- src/matrix/profile.ts | 51 +++++++++++++++++++ .../room/{create.ts => RoomBeingCreated.ts} | 41 +-------------- src/matrix/room/RoomStatus.ts | 24 --------- src/matrix/room/{common.js => common.ts} | 15 ++++++ src/matrix/room/members/RoomMember.js | 2 +- src/matrix/room/sending/PendingEvent.js | 2 +- src/matrix/room/sending/SendQueue.js | 2 +- src/matrix/room/timeline/Timeline.js | 2 +- .../room/timeline/entries/BaseEventEntry.js | 2 +- .../room/timeline/entries/EventEntry.js | 2 +- .../timeline/persistence/RelationWriter.js | 2 +- src/matrix/room/timeline/relations.js | 2 +- 17 files changed, 82 insertions(+), 77 deletions(-) create mode 100644 src/matrix/profile.ts rename src/matrix/room/{create.ts => RoomBeingCreated.ts} (86%) delete mode 100644 src/matrix/room/RoomStatus.ts rename src/matrix/room/{common.js => common.ts} (81%) diff --git a/src/domain/session/CreateRoomViewModel.js b/src/domain/session/CreateRoomViewModel.js index 6e207e31..9d2835c2 100644 --- a/src/domain/session/CreateRoomViewModel.js +++ b/src/domain/session/CreateRoomViewModel.js @@ -16,7 +16,7 @@ limitations under the License. import {ViewModel} from "../ViewModel.js"; import {imageToInfo} from "./common.js"; -import {RoomType} from "../../matrix/room/create"; +import {RoomType} from "../../matrix/room/common"; export class CreateRoomViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/RoomViewModelObservable.js b/src/domain/session/RoomViewModelObservable.js index 8fd0daf3..52833332 100644 --- a/src/domain/session/RoomViewModelObservable.js +++ b/src/domain/session/RoomViewModelObservable.js @@ -15,7 +15,7 @@ limitations under the License. */ import {ObservableValue} from "../../observable/ObservableValue"; -import {RoomStatus} from "../../matrix/room/RoomStatus"; +import {RoomStatus} from "../../matrix/room/common"; /** Depending on the status of a room (invited, joined, archived, or none), diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index f5169afa..f6cbd747 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -15,7 +15,7 @@ limitations under the License. */ import {ViewModel} from "../../ViewModel.js"; -import {RoomType} from "../../../matrix/room/create"; +import {RoomType} from "../../../matrix/room/common"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; export class MemberDetailsViewModel extends ViewModel { diff --git a/src/matrix/Session.js b/src/matrix/Session.js index a5f48bf3..c5b5ad0d 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -17,8 +17,8 @@ limitations under the License. import {Room} from "./room/Room.js"; import {ArchivedRoom} from "./room/ArchivedRoom.js"; -import {RoomStatus} from "./room/RoomStatus"; -import {RoomBeingCreated} from "./room/create"; +import {RoomStatus} from "./room/common"; +import {RoomBeingCreated} from "./room/RoomBeingCreated"; import {Invite} from "./room/Invite.js"; import {Pusher} from "./push/Pusher"; import { ObservableMap } from "../observable/index.js"; diff --git a/src/matrix/common.js b/src/matrix/common.js index 67a95205..ba7876ed 100644 --- a/src/matrix/common.js +++ b/src/matrix/common.js @@ -34,4 +34,4 @@ export function tests() { assert(!isTxnId("$yS_n5n3cIO2aTtek0_2ZSlv-7g4YYR2zKrk2mFCW_rm")); }, } -} \ No newline at end of file +} diff --git a/src/matrix/profile.ts b/src/matrix/profile.ts new file mode 100644 index 00000000..a95b2139 --- /dev/null +++ b/src/matrix/profile.ts @@ -0,0 +1,51 @@ +/* +Copyright 2020 Bruno Windels + +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. +*/ + +import type {HomeServerApi} from "./net/HomeServerApi"; +import type {ILogItem} from "../logging/types"; + +export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { + const profiles = await Promise.all(userIds.map(async userId => { + const response = await hsApi.profile(userId, {log}).response(); + return new Profile(userId, response.displayname as string, response.avatar_url as string); + })); + profiles.sort((a, b) => a.name.localeCompare(b.name)); + return profiles; +} + +export interface IProfile { + get userId(): string; + get displayName(): string | undefined; + get avatarUrl(): string | undefined; + get name(): string; +} + +export class Profile implements IProfile { + constructor( + public readonly userId: string, + public readonly displayName: string, + public readonly avatarUrl: string | undefined + ) {} + + get name() { return this.displayName || this.userId; } +} + +export class UserIdProfile implements IProfile { + constructor(public readonly userId: string) {} + get displayName() { return undefined; } + get name() { return this.userId; } + get avatarUrl() { return undefined; } +} diff --git a/src/matrix/room/create.ts b/src/matrix/room/RoomBeingCreated.ts similarity index 86% rename from src/matrix/room/create.ts rename to src/matrix/room/RoomBeingCreated.ts index c50aba6b..1ad84a47 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/RoomBeingCreated.ts @@ -19,6 +19,8 @@ import {createRoomEncryptionEvent} from "../e2ee/common"; import {MediaRepository} from "../net/MediaRepository"; import {EventEmitter} from "../../utils/EventEmitter"; import {AttachmentUpload} from "./AttachmentUpload"; +import {loadProfiles, Profile, UserIdProfile} from "../profile"; +import {RoomType} from "./common"; import type {HomeServerApi} from "../net/HomeServerApi"; import type {ILogItem} from "../../logging/types"; @@ -60,12 +62,6 @@ type Options = { alias?: string; } -export enum RoomType { - DirectMessage, - Private, - Public -} - function defaultE2EEStatusForType(type: RoomType): boolean { switch (type) { case RoomType.DirectMessage: @@ -222,36 +218,3 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { } } } - -export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { - const profiles = await Promise.all(userIds.map(async userId => { - const response = await hsApi.profile(userId, {log}).response(); - return new Profile(userId, response.displayname as string, response.avatar_url as string); - })); - profiles.sort((a, b) => a.name.localeCompare(b.name)); - return profiles; -} - -interface IProfile { - get userId(): string; - get displayName(): string | undefined; - get avatarUrl(): string | undefined; - get name(): string; -} - -export class Profile implements IProfile { - constructor( - public readonly userId: string, - public readonly displayName: string, - public readonly avatarUrl: string | undefined - ) {} - - get name() { return this.displayName || this.userId; } -} - -class UserIdProfile implements IProfile { - constructor(public readonly userId: string) {} - get displayName() { return undefined; } - get name() { return this.userId; } - get avatarUrl() { return undefined; } -} diff --git a/src/matrix/room/RoomStatus.ts b/src/matrix/room/RoomStatus.ts deleted file mode 100644 index f66f59d7..00000000 --- a/src/matrix/room/RoomStatus.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -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. -*/ - -export enum RoomStatus { - None = 1 << 0, - BeingCreated = 1 << 1, - Invited = 1 << 2, - Joined = 1 << 3, - Replaced = 1 << 4, - Archived = 1 << 5, -} diff --git a/src/matrix/room/common.js b/src/matrix/room/common.ts similarity index 81% rename from src/matrix/room/common.js rename to src/matrix/room/common.ts index b009a89c..57ab7023 100644 --- a/src/matrix/room/common.js +++ b/src/matrix/room/common.ts @@ -25,3 +25,18 @@ export const REDACTION_TYPE = "m.room.redaction"; export function isRedacted(event) { return !!event?.unsigned?.redacted_because; } + +export enum RoomStatus { + None = 1 << 0, + BeingCreated = 1 << 1, + Invited = 1 << 2, + Joined = 1 << 3, + Replaced = 1 << 4, + Archived = 1 << 5, +} + +export enum RoomType { + DirectMessage, + Private, + Public +} diff --git a/src/matrix/room/members/RoomMember.js b/src/matrix/room/members/RoomMember.js index 78096060..dabff972 100644 --- a/src/matrix/room/members/RoomMember.js +++ b/src/matrix/room/members/RoomMember.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {getPrevContentFromStateEvent} from "../common.js"; +import {getPrevContentFromStateEvent} from "../common"; export const EVENT_TYPE = "m.room.member"; diff --git a/src/matrix/room/sending/PendingEvent.js b/src/matrix/room/sending/PendingEvent.js index d4c10704..1b3567a7 100644 --- a/src/matrix/room/sending/PendingEvent.js +++ b/src/matrix/room/sending/PendingEvent.js @@ -15,7 +15,7 @@ limitations under the License. */ import {createEnum} from "../../../utils/enum"; import {AbortError} from "../../../utils/error"; -import {REDACTION_TYPE} from "../common.js"; +import {REDACTION_TYPE} from "../common"; import {getRelationFromContent, getRelationTarget, setRelationTarget} from "../timeline/relations.js"; export const SendStatus = createEnum( diff --git a/src/matrix/room/sending/SendQueue.js b/src/matrix/room/sending/SendQueue.js index 791ed854..f9950c01 100644 --- a/src/matrix/room/sending/SendQueue.js +++ b/src/matrix/room/sending/SendQueue.js @@ -18,7 +18,7 @@ import {SortedArray} from "../../../observable/list/SortedArray"; import {ConnectionError} from "../../error.js"; import {PendingEvent, SendStatus} from "./PendingEvent.js"; import {makeTxnId, isTxnId} from "../../common.js"; -import {REDACTION_TYPE} from "../common.js"; +import {REDACTION_TYPE} from "../common"; import {getRelationFromContent, getRelationTarget, setRelationTarget, REACTION_TYPE, ANNOTATION_RELATION_TYPE} from "../timeline/relations.js"; export class SendQueue { diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 90ec29eb..19e9e647 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -22,7 +22,7 @@ import {TimelineReader} from "./persistence/TimelineReader.js"; import {PendingEventEntry} from "./entries/PendingEventEntry.js"; import {RoomMember} from "../members/RoomMember.js"; import {getRelation, ANNOTATION_RELATION_TYPE} from "./relations.js"; -import {REDACTION_TYPE} from "../common.js"; +import {REDACTION_TYPE} from "../common"; import {NonPersistedEventEntry} from "./entries/NonPersistedEventEntry.js"; import {DecryptionSource} from "../../e2ee/common.js"; import {EVENT_TYPE as MEMBER_EVENT_TYPE} from "../members/RoomMember.js"; diff --git a/src/matrix/room/timeline/entries/BaseEventEntry.js b/src/matrix/room/timeline/entries/BaseEventEntry.js index ec20f48a..44fdcaec 100644 --- a/src/matrix/room/timeline/entries/BaseEventEntry.js +++ b/src/matrix/room/timeline/entries/BaseEventEntry.js @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseEntry} from "./BaseEntry"; -import {REDACTION_TYPE} from "../../common.js"; +import {REDACTION_TYPE} from "../../common"; import {createAnnotation, ANNOTATION_RELATION_TYPE, getRelationFromContent} from "../relations.js"; import {PendingAnnotation} from "../PendingAnnotation.js"; import {createReplyContent} from "./reply.js" diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 7b957e01..d218a598 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseEventEntry} from "./BaseEventEntry.js"; -import {getPrevContentFromStateEvent, isRedacted} from "../../common.js"; +import {getPrevContentFromStateEvent, isRedacted} from "../../common"; import {getRelationFromContent, getRelatedEventId} from "../relations.js"; export class EventEntry extends BaseEventEntry { diff --git a/src/matrix/room/timeline/persistence/RelationWriter.js b/src/matrix/room/timeline/persistence/RelationWriter.js index 92f97671..ae078bfc 100644 --- a/src/matrix/room/timeline/persistence/RelationWriter.js +++ b/src/matrix/room/timeline/persistence/RelationWriter.js @@ -15,7 +15,7 @@ limitations under the License. */ import {EventEntry} from "../entries/EventEntry.js"; -import {REDACTION_TYPE, isRedacted} from "../../common.js"; +import {REDACTION_TYPE, isRedacted} from "../../common"; import {ANNOTATION_RELATION_TYPE, getRelation} from "../relations.js"; import {redactEvent} from "../common.js"; diff --git a/src/matrix/room/timeline/relations.js b/src/matrix/room/timeline/relations.js index 4009d8c4..2183a6c5 100644 --- a/src/matrix/room/timeline/relations.js +++ b/src/matrix/room/timeline/relations.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {REDACTION_TYPE} from "../common.js"; +import {REDACTION_TYPE} from "../common"; export const REACTION_TYPE = "m.reaction"; export const ANNOTATION_RELATION_TYPE = "m.annotation"; From 3adb2c32545d435769f72d087473a48113df2051 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 16:44:40 +0100 Subject: [PATCH 150/160] fix ts errors --- src/logging/NullLogger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logging/NullLogger.ts b/src/logging/NullLogger.ts index 076bb313..21c3d349 100644 --- a/src/logging/NullLogger.ts +++ b/src/logging/NullLogger.ts @@ -67,7 +67,7 @@ export class NullLogItem implements ILogItem { log(): ILogItem { return this; } - set(): void {} + set(): ILogItem { return this; } runDetached(_: LabelOrValues, callback: LogCallback): ILogItem { new Promise(r => r(callback(this))).then(noop, noop); From ff46d382acc476d06a8522c981686cee38f869ef Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 19:54:15 +0100 Subject: [PATCH 151/160] adjust m.direct when creating a DM --- src/matrix/Session.js | 2 +- src/matrix/net/HomeServerApi.ts | 3 +++ src/matrix/room/RoomBeingCreated.ts | 35 +++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index c5b5ad0d..be634754 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -624,8 +624,8 @@ export class Session { // the room id. Replace the room being created with the synced room. if (roomBeingCreated.roomId && !!this.rooms.get(roomBeingCreated.roomId)) { this._tryReplaceRoomBeingCreated(roomBeingCreated.roomId, log); + await roomBeingCreated.adjustDirectMessageMapIfNeeded(this._user, this._storage, this._hsApi, log); } - // TODO: if type is DM, then adjust the m.direct account data }); return roomBeingCreated; } diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index bf2c2c21..a6749cc4 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -286,6 +286,9 @@ export class HomeServerApi { return this._post(`/createRoom`, {}, payload, options); } + setAccountData(ownUserId: string, type: string, content: Record, options?: IRequestOptions): IHomeServerRequest { + return this._put(`/user/${encodeURIComponent(ownUserId)}/account_data/${encodeURIComponent(type)}`, {}, content, options); + } } import {Request as MockRequest} from "../../mocks/Request.js"; diff --git a/src/matrix/room/RoomBeingCreated.ts b/src/matrix/room/RoomBeingCreated.ts index 1ad84a47..9e564a87 100644 --- a/src/matrix/room/RoomBeingCreated.ts +++ b/src/matrix/room/RoomBeingCreated.ts @@ -26,6 +26,8 @@ import type {HomeServerApi} from "../net/HomeServerApi"; import type {ILogItem} from "../../logging/types"; import type {Platform} from "../../platform/web/Platform"; import type {IBlobHandle} from "../../platform/types/types"; +import type {User} from "../User"; +import type {Storage} from "../storage/idb/Storage"; type CreateRoomPayload = { is_direct?: boolean; @@ -217,4 +219,37 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { this.options.avatar.blob.dispose(); } } + + async adjustDirectMessageMapIfNeeded(user: User, storage: Storage, hsApi: HomeServerApi, log: ILogItem): Promise { + if (!this.options.invites || this.options.type !== RoomType.DirectMessage) { + return; + } + const userId = this.options.invites[0]; + const DM_MAP_TYPE = "m.direct"; + await log.wrap("set " + DM_MAP_TYPE, async log => { + try { + const txn = await storage.readWriteTxn([storage.storeNames.accountData]); + let mapEntry; + try { + mapEntry = await txn.accountData.get(DM_MAP_TYPE); + if (!mapEntry) { + mapEntry = {type: DM_MAP_TYPE, content: {}}; + } + const map = mapEntry.content; + const userRooms = map[userId]; + // this is a new room id so no need to check if it's already there + userRooms.push(this._roomId); + txn.accountData.set(mapEntry); + await txn.complete(); + } catch (err) { + txn.abort(); + throw err; + } + await hsApi.setAccountData(user.id, DM_MAP_TYPE, mapEntry.content, {log}).response(); + } catch (err) { + // we can't really do anything else but logging here + log.catch(err); + } + }); + } } From d2008a336b4e4ecc4d9bd074a3af23bd308e1049 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 19:54:47 +0100 Subject: [PATCH 152/160] fix lint errors --- src/domain/session/leftpanel/LeftPanelViewModel.js | 1 - src/domain/session/room/RoomBeingCreatedViewModel.js | 1 + src/matrix/Session.js | 2 +- src/platform/web/ui/session/room/RoomBeingCreatedView.js | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 08584d7f..843ed1ca 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -21,7 +21,6 @@ import {InviteTileViewModel} from "./InviteTileViewModel.js"; import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js"; import {RoomFilter} from "./RoomFilter.js"; import {ApplyMap} from "../../../observable/map/ApplyMap.js"; -import {LogMap} from "../../../observable/map/LogMap.js"; import {addPanelIfNeeded} from "../../navigation/index.js"; export class LeftPanelViewModel extends ViewModel { diff --git a/src/domain/session/room/RoomBeingCreatedViewModel.js b/src/domain/session/room/RoomBeingCreatedViewModel.js index 0ed82be9..f98c86f9 100644 --- a/src/domain/session/room/RoomBeingCreatedViewModel.js +++ b/src/domain/session/room/RoomBeingCreatedViewModel.js @@ -43,6 +43,7 @@ export class RoomBeingCreatedViewModel extends ViewModel { return error.message; } } + return ""; } get avatarLetter() { return avatarInitials(this.name); } get avatarColorNumber() { return getIdentifierColorNumber(this._roomBeingCreated.avatarColorId); } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index be634754..287c48ea 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -606,7 +606,7 @@ export class Session { return this._roomsBeingCreated; } - createRoom(options, log = undefined) { + createRoom(options) { let roomBeingCreated; this._platform.logger.runDetached("create room", async log => { const id = `local-${Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)}`; diff --git a/src/platform/web/ui/session/room/RoomBeingCreatedView.js b/src/platform/web/ui/session/room/RoomBeingCreatedView.js index 1da44d34..3ecfde69 100644 --- a/src/platform/web/ui/session/room/RoomBeingCreatedView.js +++ b/src/platform/web/ui/session/room/RoomBeingCreatedView.js @@ -18,7 +18,6 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView"; import {LoadingView} from "../../general/LoadingView"; import {AvatarView} from "../../AvatarView"; -import {renderStaticAvatar} from "../../avatar.js"; export class RoomBeingCreatedView extends TemplateView { render(t, vm) { From 2765f48a640f5a5474cb9f71463f3ad33a640d41 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 19:59:44 +0100 Subject: [PATCH 153/160] create user id array in m.direct if it doesn't exist already --- src/matrix/room/RoomBeingCreated.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/RoomBeingCreated.ts b/src/matrix/room/RoomBeingCreated.ts index 9e564a87..78202203 100644 --- a/src/matrix/room/RoomBeingCreated.ts +++ b/src/matrix/room/RoomBeingCreated.ts @@ -236,7 +236,10 @@ export class RoomBeingCreated extends EventEmitter<{change: never}> { mapEntry = {type: DM_MAP_TYPE, content: {}}; } const map = mapEntry.content; - const userRooms = map[userId]; + let userRooms = map[userId]; + if (!userRooms) { + map[userId] = userRooms = []; + } // this is a new room id so no need to check if it's already there userRooms.push(this._roomId); txn.accountData.set(mapEntry); From d65b25f08440c88008b8751b1edac7957fdf7ee9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 20:00:01 +0100 Subject: [PATCH 154/160] also adjust m.direct if the room has already been replaced --- src/matrix/Session.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 287c48ea..778d2866 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -622,8 +622,10 @@ export class Session { await Promise.all(promises); // we should now know the roomId, check if the room was synced before we received // the room id. Replace the room being created with the synced room. - if (roomBeingCreated.roomId && !!this.rooms.get(roomBeingCreated.roomId)) { - this._tryReplaceRoomBeingCreated(roomBeingCreated.roomId, log); + if (roomBeingCreated.roomId) { + if (!!this.rooms.get(roomBeingCreated.roomId)) { + this._tryReplaceRoomBeingCreated(roomBeingCreated.roomId, log); + } await roomBeingCreated.adjustDirectMessageMapIfNeeded(this._user, this._storage, this._hsApi, log); } }); From 175f869c836f26fed2c090eb50ab34b3a8b45141 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 10 Feb 2022 20:07:27 +0100 Subject: [PATCH 155/160] fix lint --- src/matrix/Session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 778d2866..83a2df02 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -623,7 +623,7 @@ export class Session { // we should now know the roomId, check if the room was synced before we received // the room id. Replace the room being created with the synced room. if (roomBeingCreated.roomId) { - if (!!this.rooms.get(roomBeingCreated.roomId)) { + if (this.rooms.get(roomBeingCreated.roomId)) { this._tryReplaceRoomBeingCreated(roomBeingCreated.roomId, log); } await roomBeingCreated.adjustDirectMessageMapIfNeeded(this._user, this._storage, this._hsApi, log); From 57b15426882545d65a6567fa2f7f598437b807cf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 11 Feb 2022 09:37:56 +0100 Subject: [PATCH 156/160] use private topic field as public one got removed as not needed in view --- src/domain/session/CreateRoomViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/CreateRoomViewModel.js b/src/domain/session/CreateRoomViewModel.js index 9d2835c2..51a9b7a4 100644 --- a/src/domain/session/CreateRoomViewModel.js +++ b/src/domain/session/CreateRoomViewModel.js @@ -91,7 +91,7 @@ export class CreateRoomViewModel extends ViewModel { const roomBeingCreated = this._session.createRoom({ type: this.isPublic ? RoomType.Public : RoomType.Private, name: this._name ?? undefined, - topic: this.topic ?? undefined, + topic: this._topic ?? undefined, isEncrypted: !this.isPublic && this._isEncrypted, isFederationDisabled: this._isFederationDisabled, alias: this.isPublic ? ensureAliasIsLocalPart(this._roomAlias) : undefined, From ea8f3e5a6a3bdcec6f98f741010edfeea7b8f8bd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 11 Feb 2022 17:14:56 +0100 Subject: [PATCH 157/160] remove argument that is already bound in BaseRoom, making decryption fail --- src/matrix/room/timeline/Timeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 19e9e647..c6852492 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -382,7 +382,7 @@ export class Timeline { }; const eventEntry = new NonPersistedEventEntry(entry, this._fragmentIdComparer); if (this._decryptEntries) { - const request = this._decryptEntries(DecryptionSource.Timeline, [eventEntry]); + const request = this._decryptEntries([eventEntry]); await request.complete(); } return eventEntry; From 750f3cd8ff7073f8ad3bce7d5b620eac7812bbac Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 11 Feb 2022 17:20:54 +0100 Subject: [PATCH 158/160] release v0.2.26 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 18122ed2..8fa27f47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.25", + "version": "0.2.26", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "directories": { "doc": "doc" From 9685ef4dd3feda52163f405f72fcd314d6b232e6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 11 Feb 2022 18:39:37 +0100 Subject: [PATCH 159/160] dont log summary valued, as they can contain PII --- src/matrix/room/Room.js | 2 +- src/matrix/room/RoomSummary.js | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index a5bce14f..12c17580 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -158,7 +158,7 @@ export class Room extends BaseRoom { summaryChanges = this._summary.writeData(summaryChanges, txn); } if (summaryChanges) { - log.set("summaryChanges", summaryChanges.diff(this._summary.data)); + log.set("summaryChanges", summaryChanges.changedKeys(this._summary.data)); } // fetch new members while we have txn open, // but don't make any in-memory changes yet diff --git a/src/matrix/room/RoomSummary.js b/src/matrix/room/RoomSummary.js index 410352ca..a3dec467 100644 --- a/src/matrix/room/RoomSummary.js +++ b/src/matrix/room/RoomSummary.js @@ -203,16 +203,11 @@ export class SummaryData { this.cloned = copy ? true : false; } - diff(other) { + changedKeys(other) { const props = Object.getOwnPropertyNames(this); - return props.reduce((diff, prop) => { - if (prop !== "cloned") { - if (this[prop] !== other[prop]) { - diff[prop] = this[prop]; - } - } - return diff; - }, {}); + return props.filter(prop => { + return prop !== "cloned" && this[prop] !== other[prop] + }); } cloneIfNeeded() { From dfed04166e29a9dc61ff645bd4b0ad6f7f9595f3 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 11 Feb 2022 19:47:24 -0600 Subject: [PATCH 160/160] Fix missing reply text when message body is parsed as HTML in [`linkedom`](https://github.com/WebReflection/linkedom) (SSR). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [`linkedom`](https://github.com/WebReflection/linkedom) is being used https://github.com/matrix-org/matrix-public-archive to server-side render (SSR) Hydrogen (`hydrogen-view-sdk`) - This is being fixed by using a explicit HTML wrapper boilerplate with `DOMParser` to get a matching result in the browser and `linkedom`. Currently `parseHTML` is only used for HTML content bodies in events. Events with replies have content bodies that look like `Hello What's up` so they're parsed as HTML to strip out the `` part. Before | After --- | --- ![](https://user-images.githubusercontent.com/558581/153692011-2f0e7114-fcb4-481f-b217-49f461b1740a.png) | ![](https://user-images.githubusercontent.com/558581/153692016-52582fdb-abd9-439d-9dce-3f04da6959db.png) Before: ```js // Browser (Chrome, Firefox) new DOMParser().parseFromString(`

foo
`, "text/html").body.outerHTML; // '
foo
' // `linkedom` ❌ new DOMParser().parseFromString(`
foo
`, "text/html").body.outerHTML; // '' ``` After (consistent matching output): ```js // Browser (Chrome, Firefox) new DOMParser().parseFromString(`
foo
`, "text/html").body.outerHTML; // '
foo
' // `linkedom` new DOMParser().parseFromString(`
foo
`, "text/html").body.outerHTML; // '
foo
' ``` `linkedom` goal is to be close to the current DOM standard, but [not too close](https://github.com/WebReflection/linkedom#faq). Focused on the streamlined cases for server-side rendering (SSR). Here is some context around getting `DOMParser` to interpret things better. The conclusion was to only support the explicit standard cases with a `` specified instead of adding the magic HTML document creation and massaging that the browser does. - https://github.com/WebReflection/linkedom/issues/106 - https://github.com/WebReflection/linkedom/pull/108 --- Part of https://github.com/vector-im/hydrogen-web/pull/653 to support server-side rendering Hydrogen for the [`matrix-public-archive`](https://github.com/matrix-org/matrix-public-archive) project. --- src/platform/web/parsehtml.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/parsehtml.js b/src/platform/web/parsehtml.js index 21c8f39a..e80ec995 100644 --- a/src/platform/web/parsehtml.js +++ b/src/platform/web/parsehtml.js @@ -64,6 +64,6 @@ export function parseHTML(html) { // If DOMPurify uses DOMParser, can't we just get the built tree from it // instead of re-parsing? const sanitized = DOMPurify.sanitize(html, sanitizeConfig); - const bodyNode = new DOMParser().parseFromString(sanitized, "text/html").body; + const bodyNode = new DOMParser().parseFromString(`${sanitized}`, "text/html").body; return new HTMLParseResult(bodyNode); }