From 25296bb4863168d054d74877f9520f8130cd2402 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Tue, 29 Nov 2022 19:38:38 +0100 Subject: [PATCH 01/60] Update @typescript-eslint/parser (#2917) --- yarn.lock | 52 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 67c5baeb430..d39f27e5162 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1741,13 +1741,13 @@ tsutils "^3.21.0" "@typescript-eslint/parser@^5.6.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.43.0.tgz#9c86581234b88f2ba406f0b99a274a91c11630fd" - integrity sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug== + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.45.0.tgz#b18a5f6b3cf1c2b3e399e9d2df4be40d6b0ddd0e" + integrity sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ== dependencies: - "@typescript-eslint/scope-manager" "5.43.0" - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/typescript-estree" "5.43.0" + "@typescript-eslint/scope-manager" "5.45.0" + "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/typescript-estree" "5.45.0" debug "^4.3.4" "@typescript-eslint/scope-manager@5.43.0": @@ -1758,6 +1758,14 @@ "@typescript-eslint/types" "5.43.0" "@typescript-eslint/visitor-keys" "5.43.0" +"@typescript-eslint/scope-manager@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.45.0.tgz#7a4ac1bfa9544bff3f620ab85947945938319a96" + integrity sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw== + dependencies: + "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/visitor-keys" "5.45.0" + "@typescript-eslint/type-utils@5.43.0": version "5.43.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.43.0.tgz#91110fb827df5161209ecca06f70d19a96030be6" @@ -1773,6 +1781,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.43.0.tgz#e4ddd7846fcbc074325293515fa98e844d8d2578" integrity sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg== +"@typescript-eslint/types@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5" + integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA== + "@typescript-eslint/typescript-estree@5.43.0": version "5.43.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.43.0.tgz#b6883e58ba236a602c334be116bfc00b58b3b9f2" @@ -1786,6 +1799,19 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d" + integrity sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ== + dependencies: + "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/visitor-keys" "5.45.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + "@typescript-eslint/utils@5.43.0": version "5.43.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.43.0.tgz#00fdeea07811dbdf68774a6f6eacfee17fcc669f" @@ -1808,6 +1834,14 @@ "@typescript-eslint/types" "5.43.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528" + integrity sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg== + dependencies: + "@typescript-eslint/types" "5.45.0" + eslint-visitor-keys "^3.3.0" + JSONStream@^1.0.3: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -3963,9 +3997,9 @@ ieee754@^1.1.4: integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + version "5.2.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" + integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" From 6611cfa2537ef14ce8c8f7d76a45dad4a4e26167 Mon Sep 17 00:00:00 2001 From: Marco Bartelt Date: Tue, 29 Nov 2022 20:23:57 +0100 Subject: [PATCH 02/60] add-privileged-users-in-room (#2892) --- spec/integ/matrix-client-methods.spec.ts | 47 +++++++++++++++--------- src/client.ts | 18 ++++++--- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index 5508ceaf031..f2ff9e6df77 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -1337,27 +1337,38 @@ describe("MatrixClient", function() { }); }); - describe("registerWithIdentityServer", () => { - it("should pass data to POST request", async () => { - const token = { - access_token: "access_token", - token_type: "Bearer", - matrix_server_name: "server_name", - expires_in: 12345, - }; + describe("setPowerLevel", () => { + it.each([ + { + userId: "alice@localhost", + expectation: { + "alice@localhost": 100, + }, + }, + { + userId: ["alice@localhost", "bob@localhost"], + expectation: { + "alice@localhost": 100, + "bob@localhost": 100, + }, + }, + ])("should modify power levels of $userId correctly", async ({ userId, expectation }) => { + const event = { + getType: () => "m.room.power_levels", + getContent: () => ({ + users: { + "alice@localhost": 50, + }, + }), + } as MatrixEvent; - httpBackend!.when("POST", "/account/register").check(req => { - expect(req.data).toStrictEqual(token); - }).respond(200, { - access_token: "at", - token: "tt", - }); + httpBackend!.when("PUT", "/state/m.room.power_levels").check(req => { + expect(req.data.users).toStrictEqual(expectation); + }).respond(200, {}); - const prom = client!.registerWithIdentityServer(token); + const prom = client!.setPowerLevel("!room_id:server", userId, 100, event); await httpBackend!.flushAllExpected(); - const resp = await prom; - expect(resp.access_token).toBe("at"); - expect(resp.token).toBe("tt"); + await prom; }); }); }); diff --git a/src/client.ts b/src/client.ts index b091a31ec45..d8bb110630f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3795,9 +3795,9 @@ export class MatrixClient extends TypedEventEmitter { let content = { - users: {}, + users: {} as Record, }; - if (event?.getType() === EventType.RoomPowerLevels) { + if (event.getType() === EventType.RoomPowerLevels) { // take a copy of the content to ensure we don't corrupt // existing client state with a failed power level change content = utils.deepCopy(event.getContent()); } - content.users[userId] = powerLevel; + if (Array.isArray(userId)) { + for (const user of userId) { + content.users[user] = powerLevel; + } + } else { + content.users[userId] = powerLevel; + } const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", { $roomId: roomId, }); From e0df53c2ed109fa97246303bbf10998d60de22ac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Nov 2022 01:11:32 +0000 Subject: [PATCH 03/60] Update dependency terser to v5.16.0 (#2919) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d39f27e5162..f13f2de890e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6530,9 +6530,9 @@ tapable@^2.2.0: integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== terser@^5.5.1: - version "5.15.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.1.tgz#8561af6e0fd6d839669c73b92bdd5777d870ed6c" - integrity sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw== + version "5.16.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.0.tgz#29362c6f5506e71545c73b069ccd199bb28f7f54" + integrity sha512-KjTV81QKStSfwbNiwlBXfcgMcOloyuRdb62/iLFPGBcVNF4EXjhdYBhYHmbJpiBrVxZhDvltE11j+LBQUxEEJg== dependencies: "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" From 3577aa98b593915dd7092092835ea6e48959d84f Mon Sep 17 00:00:00 2001 From: Germain Date: Wed, 30 Nov 2022 07:58:29 +0000 Subject: [PATCH 04/60] Fix synthesizeReceipt (#2916) --- spec/unit/notifications.spec.ts | 24 +++++++++++++++++++++-- spec/unit/read-receipt.spec.ts | 17 +++++++++++++++++ src/client.ts | 23 ++++++++++++---------- src/models/read-receipt.ts | 2 +- src/models/room.ts | 34 +++++++++++++++++++++++++-------- src/models/thread.ts | 14 +++++++++++++- 6 files changed, 92 insertions(+), 22 deletions(-) diff --git a/spec/unit/notifications.spec.ts b/spec/unit/notifications.spec.ts index def7ef820cd..d7936014b7d 100644 --- a/spec/unit/notifications.spec.ts +++ b/spec/unit/notifications.spec.ts @@ -37,7 +37,7 @@ let event: MatrixEvent; let threadEvent: MatrixEvent; const ROOM_ID = "!roomId:example.org"; -let THREAD_ID; +let THREAD_ID: string; function mkPushAction(notify, highlight): IActionsObject { return { @@ -76,7 +76,7 @@ describe("fixNotificationCountOnDecryption", () => { event: true, }, mockClient); - THREAD_ID = event.getId(); + THREAD_ID = event.getId()!; threadEvent = mkEvent({ type: EventType.RoomMessage, content: { @@ -108,6 +108,16 @@ describe("fixNotificationCountOnDecryption", () => { expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1); }); + it("does not change the room count when there's no unread count", () => { + room.setUnreadNotificationCount(NotificationCountType.Total, 0); + room.setUnreadNotificationCount(NotificationCountType.Highlight, 0); + + fixNotificationCountOnDecryption(mockClient, event); + + expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0); + expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0); + }); + it("changes the thread count to highlight on decryption", () => { expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1); expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0); @@ -118,6 +128,16 @@ describe("fixNotificationCountOnDecryption", () => { expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1); }); + it("does not change the room count when there's no unread count", () => { + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0); + room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0); + + fixNotificationCountOnDecryption(mockClient, event); + + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0); + expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0); + }); + it("emits events", () => { const cb = jest.fn(); room.on(RoomEvent.UnreadNotifications, cb); diff --git a/spec/unit/read-receipt.spec.ts b/spec/unit/read-receipt.spec.ts index 2a3fbd87bcc..4443c25befc 100644 --- a/spec/unit/read-receipt.spec.ts +++ b/spec/unit/read-receipt.spec.ts @@ -20,6 +20,7 @@ import { MAIN_ROOM_TIMELINE, ReceiptType } from '../../src/@types/read_receipts' import { MatrixClient } from "../../src/client"; import { Feature, ServerSupport } from '../../src/feature'; import { EventType } from '../../src/matrix'; +import { synthesizeReceipt } from '../../src/models/read-receipt'; import { encodeUri } from '../../src/utils'; import * as utils from "../test-utils/test-utils"; @@ -175,4 +176,20 @@ describe("Read receipt", () => { await flushPromises(); }); }); + + describe("synthesizeReceipt", () => { + it.each([ + { event: roomEvent, destinationId: MAIN_ROOM_TIMELINE }, + { event: threadEvent, destinationId: threadEvent.threadRootId! }, + ])("adds the receipt to $destinationId", ({ event, destinationId }) => { + const userId = "@bob:example.org"; + const receiptType = ReceiptType.Read; + + const fakeReadReceipt = synthesizeReceipt(userId, event, receiptType); + + const content = fakeReadReceipt.getContent()[event.getId()!][receiptType][userId]; + + expect(content.thread_id).toEqual(destinationId); + }); + }); }); diff --git a/src/client.ts b/src/client.ts index d8bb110630f..8b8fa18c315 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5836,8 +5836,14 @@ export class MatrixClient extends TypedEventEmitter { const mapper = this.getEventMapper(); const matrixEvents = res.chunk.map(mapper); - for (const event of matrixEvents) { - await eventTimeline.getTimelineSet()?.thread?.processEvent(event); + + // Process latest events first + for (const event of matrixEvents.slice().reverse()) { + await thread?.processEvent(event); + const sender = event.getSender()!; + if (!backwards || thread?.getEventReadUpTo(sender) === null) { + room.addLocalEchoReceipt(sender, event, ReceiptType.Read); + } } const newToken = res.next_batch; @@ -9344,19 +9350,16 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri if (!room || !cli.getUserId()) return; const isThreadEvent = !!event.threadRootId && !event.isThreadRoot; - const currentCount = (isThreadEvent - ? room.getThreadUnreadNotificationCount( - event.threadRootId, - NotificationCountType.Highlight, - ) - : room.getUnreadNotificationCount(NotificationCountType.Highlight)) ?? 0; + + const totalCount = room.getUnreadCountForEventContext(NotificationCountType.Total, event); + const currentCount = room.getUnreadCountForEventContext(NotificationCountType.Highlight, event); // Ensure the unread counts are kept up to date if the event is encrypted // We also want to make sure that the notification count goes up if we already // have encrypted events to avoid other code from resetting 'highlight' to zero. const oldHighlight = !!oldActions?.tweaks?.highlight; const newHighlight = !!actions?.tweaks?.highlight; - if (oldHighlight !== newHighlight || currentCount > 0) { + if ((oldHighlight !== newHighlight || currentCount > 0) && totalCount > 0) { // TODO: Handle mentions received while the client is offline // See also https://github.com/vector-im/element-web/issues/9069 const hasReadEvent = isThreadEvent @@ -9381,7 +9384,7 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri // Fix 'Mentions Only' rooms from not having the right badge count const totalCount = (isThreadEvent ? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total) - : room.getUnreadNotificationCount(NotificationCountType.Total)) ?? 0; + : room.getRoomUnreadNotificationCount(NotificationCountType.Total)) ?? 0; if (totalCount < newCount) { if (isThreadEvent) { diff --git a/src/models/read-receipt.ts b/src/models/read-receipt.ts index 3d78991478f..391cd98117e 100644 --- a/src/models/read-receipt.ts +++ b/src/models/read-receipt.ts @@ -33,7 +33,7 @@ export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptTyp [receiptType]: { [userId]: { ts: event.getTs(), - threadId: event.threadRootId ?? MAIN_ROOM_TIMELINE, + thread_id: event.threadRootId ?? MAIN_ROOM_TIMELINE, }, }, }, diff --git a/src/models/room.ts b/src/models/room.ts index 0d16940273e..de3d18733de 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -1183,7 +1183,7 @@ export class Room extends ReadReceipt { * for this type. */ public getUnreadNotificationCount(type = NotificationCountType.Total): number { - let count = this.notificationCounts[type] ?? 0; + let count = this.getRoomUnreadNotificationCount(type); if (this.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) { for (const threadNotification of this.threadNotifications.values()) { count += threadNotification[type] ?? 0; @@ -1192,6 +1192,27 @@ export class Room extends ReadReceipt { return count; } + /** + * Get the notification for the event context (room or thread timeline) + */ + public getUnreadCountForEventContext(type = NotificationCountType.Total, event: MatrixEvent): number { + const isThreadEvent = !!event.threadRootId && !event.isThreadRoot; + + return (isThreadEvent + ? this.getThreadUnreadNotificationCount(event.threadRootId, type) + : this.getRoomUnreadNotificationCount(type)) ?? 0; + } + + /** + * Get one of the notification counts for this room + * @param {String} type The type of notification count to get. default: 'total' + * @return {Number} The notification count, or undefined if there is no count + * for this type. + */ + public getRoomUnreadNotificationCount(type = NotificationCountType.Total): number { + return this.notificationCounts[type] ?? 0; + } + /** * @experimental * Get one of the notification counts for a thread @@ -1758,20 +1779,17 @@ export class Room extends ReadReceipt { let latestMyThreadsRootEvent: MatrixEvent | undefined; const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS); for (const rootEvent of threadRoots) { - this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, { + const opts = { duplicateStrategy: DuplicateStrategy.Ignore, fromCache: false, roomState, - }); + }; + this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts); const threadRelationship = rootEvent .getServerAggregatedRelation(THREAD_RELATION_TYPE.name); if (threadRelationship?.current_user_participated) { - this.threadsTimelineSets[1]?.addLiveEvent(rootEvent, { - duplicateStrategy: DuplicateStrategy.Ignore, - fromCache: false, - roomState, - }); + this.threadsTimelineSets[1]?.addLiveEvent(rootEvent, opts); latestMyThreadsRootEvent = rootEvent; } } diff --git a/src/models/thread.ts b/src/models/thread.ts index 5abe63d6c2f..8c9826c35ec 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -123,7 +123,7 @@ export class Thread extends ReadReceipt { this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); this.room.on(RoomEvent.Redaction, this.onRedaction); this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho); - this.timelineSet.on(RoomEvent.Timeline, this.onEcho); + this.timelineSet.on(RoomEvent.Timeline, this.onTimelineEvent); // even if this thread is thought to be originating from this client, we initialise it as we may be in a // gappy sync and a thread around this event may already exist. @@ -192,6 +192,18 @@ export class Thread extends ReadReceipt { } }; + private onTimelineEvent = ( + event: MatrixEvent, + room: Room | undefined, + toStartOfTimeline: boolean | undefined, + ): void => { + // Add a synthesized receipt when paginating forward in the timeline + if (!toStartOfTimeline) { + room!.addLocalEchoReceipt(event.getSender()!, event, ReceiptType.Read); + } + this.onEcho(event); + }; + private onEcho = async (event: MatrixEvent): Promise => { if (event.threadRootId !== this.id) return; // ignore echoes for other timelines if (this.lastEvent === event) return; From 8d6d262e5fa75f2e69db0d4dd0977262800cf4af Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 30 Nov 2022 09:27:23 +0000 Subject: [PATCH 05/60] Update CODEOWNERS (#2918) --- .github/CODEOWNERS | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f28b852a788..dd0bfc9ebfa 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,6 @@ -* @matrix-org/element-web - -/src/webrtc @matrix-org/element-call-reviewers -/spec/*/webrtc @matrix-org/element-call-reviewers +* @matrix-org/element-web +/.github/workflows/** @matrix-org/element-web-app-team +/package.json @matrix-org/element-web-app-team +/yarn.lock @matrix-org/element-web-app-team +/src/webrtc @matrix-org/element-call-reviewers +/spec/*/webrtc @matrix-org/element-call-reviewers From 1606274c36008b6a976a5e4b47cdd13a1e4e5997 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 30 Nov 2022 10:53:38 +0000 Subject: [PATCH 06/60] Process `m.room.encryption` events before emitting `RoomMember` events (#2914) vector-im/element-web#23819 is an intermittent failure to correctly initiate a user verification process. The root cause is as follows: * In matrix-react-sdk, ensureDMExists tries to create an encrypted DM room, and assumes it is ready for use (including sending encrypted events) as soon as it receives a RoomStateEvent.NewMember notification indicating that the other user has been invited or joined. * However, in sync.ts, we process the membership events in a /sync response (including emitting RoomStateEvent.NewMember notifications), which is long before we process any m.room.encryption event. * The upshot is that we can end up trying to send an encrypted event in the new room before processing the m.room.encryption event, which causes the crypto layer to blow up with an error of "Room was previously configured to use encryption, but is no longer". Strictly speaking, ensureDMExists probably ought to be listening for ClientEvent.Room as well as RoomStateEvent.NewMember; but that doesn't help us, because ClientEvent.Room is also emitted before we process the crypto event. So, we need to process the crypto event before we start emitting these other events; but a corollary of that is that we need to do so before we store the new room in the client's store. That makes things tricky, because currently the crypto layer expects the room to have been stored in the client first. So... we have to rearrange everything to pass the newly-created Room object into the crypto layer, rather than just the room id, so that it doesn't need to rely on getting the Room from the client's store. --- spec/integ/megolm-integ.spec.ts | 110 ++++++++++++++++++++++++++++++-- spec/test-utils/test-utils.ts | 24 ++++--- spec/unit/crypto.spec.ts | 45 +++++++++++++ src/crypto/index.ts | 90 +++++++++++++++++--------- src/sliding-sync-sdk.ts | 2 +- src/sync.ts | 32 +++++----- 6 files changed, 243 insertions(+), 60 deletions(-) diff --git a/spec/integ/megolm-integ.spec.ts b/spec/integ/megolm-integ.spec.ts index a4891b702f6..89c06426617 100644 --- a/spec/integ/megolm-integ.spec.ts +++ b/spec/integ/megolm-integ.spec.ts @@ -21,16 +21,19 @@ import * as testUtils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; import { logger } from "../../src/logger"; import { + IClaimOTKsResult, IContent, + IDownloadKeyResult, IEvent, - IClaimOTKsResult, IJoinedRoom, + IndexedDBCryptoStore, ISyncResponse, - IDownloadKeyResult, + IUploadKeysRequest, MatrixEvent, MatrixEventEvent, - IndexedDBCryptoStore, Room, + RoomMember, + RoomStateEvent, } from "../../src/matrix"; import { IDeviceKeys } from "../../src/crypto/dehydration"; import { DeviceInfo } from "../../src/crypto/deviceinfo"; @@ -327,7 +330,9 @@ describe("megolm", () => { const room = aliceTestClient.client.getRoom(ROOM_ID)!; const event = room.getLiveTimeline().getEvents()[0]; expect(event.isEncrypted()).toBe(true); - const decryptedEvent = await testUtils.awaitDecryption(event); + + // it probably won't be decrypted yet, because it takes a while to process the olm keys + const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true }); expect(decryptedEvent.getContent().body).toEqual('42'); }); @@ -873,7 +878,12 @@ describe("megolm", () => { const room = aliceTestClient.client.getRoom(ROOM_ID)!; await room.decryptCriticalEvents(); - expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42'); + + // it probably won't be decrypted yet, because it takes a while to process the olm keys + const decryptedEvent = await testUtils.awaitDecryption( + room.getLiveTimeline().getEvents()[0], { waitOnDecryptionFailure: true }, + ); + expect(decryptedEvent.getContent().body).toEqual('42'); const exported = await aliceTestClient.client.exportRoomKeys(); @@ -1012,7 +1022,9 @@ describe("megolm", () => { const room = aliceTestClient.client.getRoom(ROOM_ID)!; const event = room.getLiveTimeline().getEvents()[0]; expect(event.isEncrypted()).toBe(true); - const decryptedEvent = await testUtils.awaitDecryption(event); + + // it probably won't be decrypted yet, because it takes a while to process the olm keys + const decryptedEvent = await testUtils.awaitDecryption(event, { waitOnDecryptionFailure: true }); expect(decryptedEvent.getRoomId()).toEqual(ROOM_ID); expect(decryptedEvent.getContent()).toEqual({}); expect(decryptedEvent.getClearContent()).toBeUndefined(); @@ -1364,4 +1376,90 @@ describe("megolm", () => { await beccaTestClient.stop(); }); + + it("allows sending an encrypted event as soon as room state arrives", async () => { + /* Empirically, clients expect to be able to send encrypted events as soon as the + * RoomStateEvent.NewMember notification is emitted, so test that works correctly. + */ + const testRoomId = "!testRoom:id"; + await aliceTestClient.start(); + + aliceTestClient.httpBackend.when("POST", "/keys/query") + .respond(200, function(_path, content: IUploadKeysRequest) { + return { device_keys: {} }; + }); + + /* Alice makes the /createRoom call */ + aliceTestClient.httpBackend.when("POST", "/createRoom") + .respond(200, { room_id: testRoomId }); + await Promise.all([ + aliceTestClient.client.createRoom({ + initial_state: [{ + type: 'm.room.encryption', + state_key: '', + content: { algorithm: 'm.megolm.v1.aes-sha2' }, + }], + }), + aliceTestClient.httpBackend.flushAllExpected(), + ]); + + /* The sync arrives in two parts; first the m.room.create... */ + aliceTestClient.httpBackend.when("GET", "/sync").respond(200, { + rooms: { join: { + [testRoomId]: { + timeline: { events: [ + { + type: 'm.room.create', + state_key: '', + event_id: "$create", + }, + { + type: 'm.room.member', + state_key: aliceTestClient.getUserId(), + content: { membership: "join" }, + event_id: "$alijoin", + }, + ] }, + }, + } }, + }); + await aliceTestClient.flushSync(); + + // ... and then the e2e event and an invite ... + aliceTestClient.httpBackend.when("GET", "/sync").respond(200, { + rooms: { join: { + [testRoomId]: { + timeline: { events: [ + { + type: 'm.room.encryption', + state_key: '', + content: { algorithm: 'm.megolm.v1.aes-sha2' }, + event_id: "$e2e", + }, + { + type: 'm.room.member', + state_key: "@other:user", + content: { membership: "invite" }, + event_id: "$otherinvite", + }, + ] }, + }, + } }, + }); + + // as soon as the roomMember arrives, try to send a message + aliceTestClient.client.on(RoomStateEvent.NewMember, (_e, _s, member: RoomMember) => { + if (member.userId == "@other:user") { + aliceTestClient.client.sendMessage(testRoomId, { msgtype: "m.text", body: "Hello, World" }); + } + }); + + // flush the sync and wait for the /send/ request. + aliceTestClient.httpBackend.when("PUT", "/send/m.room.encrypted/") + .respond(200, (_path, _content) => ({ event_id: "asdfgh" })); + await Promise.all([ + aliceTestClient.flushSync(), + aliceTestClient.httpBackend.flush("/send/m.room.encrypted/", 1), + ]); + }); }); diff --git a/spec/test-utils/test-utils.ts b/spec/test-utils/test-utils.ts index af87ebbe64f..21ae4d18bad 100644 --- a/spec/test-utils/test-utils.ts +++ b/spec/test-utils/test-utils.ts @@ -362,22 +362,28 @@ export class MockStorageApi { * @param {MatrixEvent} event * @returns {Promise} promise which resolves (to `event`) when the event has been decrypted */ -export async function awaitDecryption(event: MatrixEvent): Promise { +export async function awaitDecryption( + event: MatrixEvent, { waitOnDecryptionFailure = false } = {}, +): Promise { // An event is not always decrypted ahead of time // getClearContent is a good signal to know whether an event has been decrypted // already if (event.getClearContent() !== null) { - return event; + if (waitOnDecryptionFailure && event.isDecryptionFailure()) { + logger.log(`${Date.now()} event ${event.getId()} got decryption error; waiting`); + } else { + return event; + } } else { - logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`); + logger.log(`${Date.now()} event ${event.getId()} is not yet decrypted; waiting`); + } - return new Promise((resolve) => { - event.once(MatrixEventEvent.Decrypted, (ev) => { - logger.log(`${Date.now()} event ${event.getId()} now decrypted`); - resolve(ev); - }); + return new Promise((resolve) => { + event.once(MatrixEventEvent.Decrypted, (ev) => { + logger.log(`${Date.now()} event ${event.getId()} now decrypted`); + resolve(ev); }); - } + }); } export const emitPromise = (e: EventEmitter, k: string): Promise => new Promise(r => e.once(k, r)); diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index e8a95370c61..68c8264b6f8 100644 --- a/spec/unit/crypto.spec.ts +++ b/spec/unit/crypto.spec.ts @@ -19,6 +19,7 @@ import { MemoryStore } from "../../src"; import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager'; import { RoomMember } from '../../src/models/room-member'; import { IStore } from '../../src/store'; +import { IRoomEncryption, RoomList } from "../../src/crypto/RoomList"; const Olm = global.Olm; @@ -1140,4 +1141,48 @@ describe("Crypto", function() { await client!.client.crypto!.start(); }); }); + + describe("setRoomEncryption", () => { + let mockClient: MatrixClient; + let mockRoomList: RoomList; + let clientStore: IStore; + let crypto: Crypto; + + beforeEach(async function() { + mockClient = {} as MatrixClient; + const mockStorage = new MockStorageApi() as unknown as Storage; + clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore; + const cryptoStore = new MemoryCryptoStore(); + + mockRoomList = { + getRoomEncryption: jest.fn().mockReturnValue(null), + setRoomEncryption: jest.fn().mockResolvedValue(undefined), + } as unknown as RoomList; + + crypto = new Crypto( + mockClient, + "@alice:home.server", + "FLIBBLE", + clientStore, + cryptoStore, + mockRoomList, + [], + ); + }); + + it("should set the algorithm if called for a known room", async () => { + const room = new Room("!room:id", mockClient, "@my.user:id"); + await clientStore.storeRoom(room); + await crypto.setRoomEncryption("!room:id", { algorithm: "m.megolm.v1.aes-sha2" } as IRoomEncryption); + expect(mockRoomList!.setRoomEncryption).toHaveBeenCalledTimes(1); + expect(jest.mocked(mockRoomList!.setRoomEncryption).mock.calls[0][0]).toEqual("!room:id"); + }); + + it("should raise if called for an unknown room", async () => { + await expect(async () => { + await crypto.setRoomEncryption("!room:id", { algorithm: "m.megolm.v1.aes-sha2" } as IRoomEncryption); + }).rejects.toThrow(/unknown room/); + expect(mockRoomList!.setRoomEncryption).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 4a7f73e9bd4..0f203bc25cc 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -86,6 +86,7 @@ import { ISyncStateData } from "../sync"; import { CryptoStore } from "./store/base"; import { IVerificationChannel } from "./verification/request/Channel"; import { TypedEventEmitter } from "../models/typed-event-emitter"; +import { IContent } from "../models/event"; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -2552,12 +2553,45 @@ export class Crypto extends TypedEventEmitter { + const room = this.clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to enable encryption tracking devices in unknown room ${roomId}`); + } + await this.setRoomEncryptionImpl(room, config); + if (!this.lazyLoadMembers && !inhibitDeviceQuery) { + this.deviceList.refreshOutdatedDeviceLists(); + } + } + + /** + * Set up encryption for a room. + * + * This is called when an m.room.encryption event is received. It saves a flag + * for the room in the cryptoStore (if it wasn't already set), sets up an "encryptor" for + * the room, and enables device-list tracking for the room. + * + * It does not initiate a device list query for the room. That is normally + * done once we finish processing the sync, in onSyncCompleted. + * + * @param room The room to enable encryption in. + * @param config The encryption config for the room. + */ + private async setRoomEncryptionImpl( + room: Room, + config: IRoomEncryption, + ): Promise { + const roomId = room.roomId; + // ignore crypto events with no algorithm defined // This will happen if a crypto event is redacted before we fetch the room state // It would otherwise just throw later as an unknown algorithm would, but we may @@ -2625,14 +2659,7 @@ export class Crypto extends TypedEventEmitter { + const room = this.clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); + } + return this.trackRoomDevicesImpl(room); + } + + /** + * Make sure we are tracking the device lists for all users in this room. + * + * This is normally called when we are about to send an encrypted event, to make sure + * we have all the devices in the room; but it is also called when processing an + * m.room.encryption state event (if lazy-loading is disabled), or when members are + * loaded (if lazy-loading is enabled), to prepare the device list. + * + * @param room Room to enable device-list tracking in + */ + private trackRoomDevicesImpl(room: Room): Promise { + const roomId = room.roomId; const trackMembers = async (): Promise => { // not an encrypted room if (!this.roomEncryptors.has(roomId)) { return; } - const room = this.clientStore.getRoom(roomId); - if (!room) { - throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); - } logger.log(`Starting to track devices for room ${roomId} ...`); const members = await room.getEncryptionTargetMembers(); members.forEach((m) => { @@ -2815,17 +2857,14 @@ export class Crypto extends TypedEventEmitter { - const roomId = event.getRoomId()!; + public async onCryptoEvent(room: Room, event: MatrixEvent): Promise { const content = event.getContent(); - - try { - // inhibit the device list refresh for now - it will happen once we've - // finished processing the sync, in onSyncCompleted. - await this.setRoomEncryption(roomId, content, true); - } catch (e) { - logger.error(`Error configuring encryption in room ${roomId}`, e); - } + await this.setRoomEncryptionImpl(room, content); } /** diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index a787b4e3465..03a17bed327 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -703,7 +703,7 @@ export class SlidingSyncSdk { const processRoomEvent = async (e: MatrixEvent): Promise => { client.emit(ClientEvent.Event, e); if (e.isState() && e.getType() == EventType.RoomEncryption && this.opts.crypto) { - await this.opts.crypto.onCryptoEvent(e); + await this.opts.crypto.onCryptoEvent(room, e); } }; diff --git a/src/sync.ts b/src/sync.ts index f32ccf7d239..20cab67b6eb 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1362,6 +1362,18 @@ export class SyncApi { } } + // process any crypto events *before* emitting the RoomStateEvent events. This + // avoids a race condition if the application tries to send a message after the + // state event is processed, but before crypto is enabled, which then causes the + // crypto layer to complain. + if (this.opts.crypto) { + for (const e of stateEvents.concat(events)) { + if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") { + await this.opts.crypto.onCryptoEvent(room, e); + } + } + } + try { await this.injectRoomEvents(room, stateEvents, events, syncEventData.fromCache); } catch (e) { @@ -1389,21 +1401,11 @@ export class SyncApi { this.processEventsForNotifs(room, events); - const processRoomEvent = async (e): Promise => { - client.emit(ClientEvent.Event, e); - if (e.isState() && e.getType() == "m.room.encryption" && this.opts.crypto) { - await this.opts.crypto.onCryptoEvent(e); - } - }; - - await utils.promiseMapSeries(stateEvents, processRoomEvent); - await utils.promiseMapSeries(events, processRoomEvent); - ephemeralEvents.forEach(function(e) { - client.emit(ClientEvent.Event, e); - }); - accountDataEvents.forEach(function(e) { - client.emit(ClientEvent.Event, e); - }); + const emitEvent = (e: MatrixEvent): boolean => client.emit(ClientEvent.Event, e); + stateEvents.forEach(emitEvent); + events.forEach(emitEvent); + ephemeralEvents.forEach(emitEvent); + accountDataEvents.forEach(emitEvent); // Decrypt only the last message in all rooms to make sure we can generate a preview // And decrypt all events after the recorded read receipt to ensure an accurate From 4362297edc9dc45d154fe83f97f479a2901905aa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:21:43 +0000 Subject: [PATCH 07/60] Update dependency @typescript-eslint/eslint-plugin to v5.44.0 (#2924) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 72 +++++++++++++++---------------------------------------- 1 file changed, 19 insertions(+), 53 deletions(-) diff --git a/yarn.lock b/yarn.lock index f13f2de890e..987c5a6cdb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1726,13 +1726,13 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.6.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.43.0.tgz#4a5248eb31b454715ddfbf8cfbf497529a0a78bc" - integrity sha512-wNPzG+eDR6+hhW4yobEmpR36jrqqQv1vxBq5LJO3fBAktjkvekfr4BRl+3Fn1CM/A+s8/EiGUbOMDoYqWdbtXA== + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.0.tgz#ffa505cf961d4844d38cfa19dcec4973a6039e41" + integrity sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA== dependencies: - "@typescript-eslint/scope-manager" "5.43.0" - "@typescript-eslint/type-utils" "5.43.0" - "@typescript-eslint/utils" "5.43.0" + "@typescript-eslint/scope-manager" "5.45.0" + "@typescript-eslint/type-utils" "5.45.0" + "@typescript-eslint/utils" "5.45.0" debug "^4.3.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" @@ -1750,14 +1750,6 @@ "@typescript-eslint/typescript-estree" "5.45.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.43.0.tgz#566e46303392014d5d163704724872e1f2dd3c15" - integrity sha512-XNWnGaqAtTJsUiZaoiGIrdJYHsUOd3BZ3Qj5zKp9w6km6HsrjPk/TGZv0qMTWyWj0+1QOqpHQ2gZOLXaGA9Ekw== - dependencies: - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/visitor-keys" "5.43.0" - "@typescript-eslint/scope-manager@5.45.0": version "5.45.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.45.0.tgz#7a4ac1bfa9544bff3f620ab85947945938319a96" @@ -1766,39 +1758,21 @@ "@typescript-eslint/types" "5.45.0" "@typescript-eslint/visitor-keys" "5.45.0" -"@typescript-eslint/type-utils@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.43.0.tgz#91110fb827df5161209ecca06f70d19a96030be6" - integrity sha512-K21f+KY2/VvYggLf5Pk4tgBOPs2otTaIHy2zjclo7UZGLyFH86VfUOm5iq+OtDtxq/Zwu2I3ujDBykVW4Xtmtg== +"@typescript-eslint/type-utils@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.45.0.tgz#aefbc954c40878fcebeabfb77d20d84a3da3a8b2" + integrity sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q== dependencies: - "@typescript-eslint/typescript-estree" "5.43.0" - "@typescript-eslint/utils" "5.43.0" + "@typescript-eslint/typescript-estree" "5.45.0" + "@typescript-eslint/utils" "5.45.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.43.0.tgz#e4ddd7846fcbc074325293515fa98e844d8d2578" - integrity sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg== - "@typescript-eslint/types@5.45.0": version "5.45.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5" integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA== -"@typescript-eslint/typescript-estree@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.43.0.tgz#b6883e58ba236a602c334be116bfc00b58b3b9f2" - integrity sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg== - dependencies: - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/visitor-keys" "5.43.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - "@typescript-eslint/typescript-estree@5.45.0": version "5.45.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d" @@ -1812,28 +1786,20 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.43.0.tgz#00fdeea07811dbdf68774a6f6eacfee17fcc669f" - integrity sha512-8nVpA6yX0sCjf7v/NDfeaOlyaIIqL7OaIGOWSPFqUKK59Gnumd3Wa+2l8oAaYO2lk0sO+SbWFWRSvhu8gLGv4A== +"@typescript-eslint/utils@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.45.0.tgz#9cca2996eee1b8615485a6918a5c763629c7acf5" + integrity sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.43.0" - "@typescript-eslint/types" "5.43.0" - "@typescript-eslint/typescript-estree" "5.43.0" + "@typescript-eslint/scope-manager" "5.45.0" + "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/typescript-estree" "5.45.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.43.0": - version "5.43.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.43.0.tgz#cbbdadfdfea385310a20a962afda728ea106befa" - integrity sha512-icl1jNH/d18OVHLfcwdL3bWUKsBeIiKYTGxMJCoGe7xFht+E4QgzOqoWYrU8XSLJWhVw8nTacbm03v23J/hFTg== - dependencies: - "@typescript-eslint/types" "5.43.0" - eslint-visitor-keys "^3.3.0" - "@typescript-eslint/visitor-keys@5.45.0": version "5.45.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528" From d692a5dbe239d06e04159c40f282159725415689 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:22:05 +0000 Subject: [PATCH 08/60] Update tspascoal/get-user-teams-membership action to v2 (#2925) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pull_request.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 940aa0ea239..d2a8857aa5f 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -42,7 +42,7 @@ jobs: if: github.event.action == 'opened' steps: - name: Check membership - uses: tspascoal/get-user-teams-membership@v1 + uses: tspascoal/get-user-teams-membership@v2 id: teams with: username: ${{ github.event.pull_request.user.login }} From 720ea0e12e5fe757e598f0e493631754b9d6d02a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Dec 2022 07:24:50 +0000 Subject: [PATCH 09/60] Update all non-major dependencies (#2928) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index 987c5a6cdb3..c748b4750b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1049,6 +1049,13 @@ uuid "8.3.2" xml "1.0.1" +"@eslint-community/eslint-utils@^4.1.0": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.1.2.tgz#14ca568ddaa291dd19a4a54498badc18c6cfab78" + integrity sha512-7qELuQWWjVDdVsFQ5+beUl+KPczrEDA7S3zM4QUd/bJl7oXgsmpXaEVqrRTnOBqenOV4rWf2kVZk2Ot085zPWA== + dependencies: + eslint-visitor-keys "^3.3.0" + "@eslint/eslintrc@^1.3.3": version "1.3.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95" @@ -2556,11 +2563,16 @@ chokidar@^3.4.0: optionalDependencies: fsevents "~2.3.2" -ci-info@^3.2.0, ci-info@^3.6.1: +ci-info@^3.2.0: version "3.6.1" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.6.1.tgz#7594f1c95cb7fdfddee7af95a13af7dbc67afdcf" integrity sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w== +ci-info@^3.6.1: + version "3.7.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.0.tgz#6d01b3696c59915b6ce057e4aa4adfc2fa25f5ef" + integrity sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog== + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -3283,23 +3295,23 @@ eslint-plugin-matrix-org@^0.8.0: integrity sha512-/Poz/F8lXYDsmQa29iPSt+kO+Jn7ArvRdq10g0CCk8wbRS0sb2zb6fvd9xL1BgR5UDQL771V0l8X32etvY5yKA== eslint-plugin-unicorn@^45.0.0: - version "45.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-45.0.0.tgz#a6650ff3000dc1a87cc2f6ac3a11edcde61712e2" - integrity sha512-iP8cMRxXKHonKioOhnCoCcqVhoqhAp6rB+nsoLjXFDxTHz3btWMAp8xwzjHA0B1K6YV/U/Yvqn1bUXZt8sJPuQ== + version "45.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-45.0.1.tgz#2307f4620502fd955c819733ce1276bed705b736" + integrity sha512-tLnIw5oDJJc3ILYtlKtqOxPP64FZLTkZkgeuoN6e7x6zw+rhBjOxyvq2c7577LGxXuIhBYrwisZuKNqOOHp3BA== dependencies: "@babel/helper-validator-identifier" "^7.19.1" + "@eslint-community/eslint-utils" "^4.1.0" ci-info "^3.6.1" clean-regexp "^1.0.0" - eslint-utils "^3.0.0" esquery "^1.4.0" indent-string "^4.0.0" is-builtin-module "^3.2.0" - jsesc "3.0.2" + jsesc "^3.0.2" lodash "^4.17.21" pluralize "^8.0.0" read-pkg-up "^7.0.1" regexp-tree "^0.1.24" - regjsparser "0.9.1" + regjsparser "^0.9.1" safe-regex "^2.1.1" semver "^7.3.8" strip-indent "^3.0.0" @@ -3520,9 +3532,9 @@ ext@^1.1.2: type "^2.7.2" fake-indexeddb@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-4.0.0.tgz#1dfb2023a3be175e35a6d84975218b432041934d" - integrity sha512-oCfWSJ/qvQn1XPZ8SHX6kY3zr1t+bN7faZ/lltGY0SBGhFOPXnWf0+pbO/MOAgfMx6khC2gK3S/bvAgQpuQHDQ== + version "4.0.1" + resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-4.0.1.tgz#09bb2468e21d0832b2177e894765fb109edac8fb" + integrity sha512-hFRyPmvEZILYgdcLBxVdHLik4Tj3gDTu/g7s9ZDOiU3sTNiGx+vEu1ri/AMsFJUZ/1sdRbAVrEcKndh3sViBcA== dependencies: realistic-structured-clone "^3.0.0" @@ -4810,16 +4822,16 @@ jsdom@^20.0.0: ws "^8.9.0" xml-name-validator "^4.0.0" -jsesc@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== - jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" @@ -6005,7 +6017,7 @@ regjsgen@^0.7.1: resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.7.1.tgz#ee5ef30e18d3f09b7c369b76e7c2373ed25546f6" integrity sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA== -regjsparser@0.9.1, regjsparser@^0.9.1: +regjsparser@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== From a0f3e5d3bf1d0791d40152d8b7f388af60c30e81 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Dec 2022 07:25:08 +0000 Subject: [PATCH 10/60] Update babel monorepo (#2929) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 104 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 30 deletions(-) diff --git a/yarn.lock b/yarn.lock index c748b4750b1..77cca92702e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -58,26 +58,31 @@ dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.0", "@babel/compat-data@^7.20.1": +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1": version "7.20.1" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.1.tgz#f2e6ef7790d8c8dbf03d379502dcc246dcce0b30" integrity sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ== +"@babel/compat-data@^7.20.0": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.5.tgz#86f172690b093373a933223b4745deeb6049e733" + integrity sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g== + "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.7.5": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.2.tgz#8dc9b1620a673f92d3624bd926dc49a52cf25b92" - integrity sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g== + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.5.tgz#45e2114dc6cd4ab167f81daf7820e8fa1250d113" + integrity sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ== dependencies: "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.2" + "@babel/generator" "^7.20.5" "@babel/helper-compilation-targets" "^7.20.0" "@babel/helper-module-transforms" "^7.20.2" - "@babel/helpers" "^7.20.1" - "@babel/parser" "^7.20.2" + "@babel/helpers" "^7.20.5" + "@babel/parser" "^7.20.5" "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.1" - "@babel/types" "^7.20.2" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -109,7 +114,16 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" -"@babel/generator@^7.20.1", "@babel/generator@^7.20.2", "@babel/generator@^7.7.2": +"@babel/generator@^7.20.1", "@babel/generator@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95" + integrity sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA== + dependencies: + "@babel/types" "^7.20.5" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + +"@babel/generator@^7.7.2": version "7.20.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.4.tgz#4d9f8f0c30be75fd90a0562099a26e5839602ab8" integrity sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA== @@ -310,14 +324,14 @@ "@babel/traverse" "^7.19.0" "@babel/types" "^7.19.0" -"@babel/helpers@^7.20.1": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.1.tgz#2ab7a0fcb0a03b5bf76629196ed63c2d7311f4c9" - integrity sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg== +"@babel/helpers@^7.20.5": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.6.tgz#e64778046b70e04779dfbdf924e7ebb45992c763" + integrity sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w== dependencies: "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.1" - "@babel/types" "^7.20.0" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" "@babel/highlight@^7.18.6": version "7.18.6" @@ -328,11 +342,16 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.2.3", "@babel/parser@^7.20.1", "@babel/parser@^7.20.2": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.2.3": version "7.20.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2" integrity sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg== +"@babel/parser@^7.18.10", "@babel/parser@^7.20.1", "@babel/parser@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" + integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -995,11 +1014,11 @@ source-map-support "^0.5.16" "@babel/runtime@^7.12.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9" - integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg== + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" + integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== dependencies: - regenerator-runtime "^0.13.10" + regenerator-runtime "^0.13.11" "@babel/template@^7.18.10", "@babel/template@^7.3.3": version "7.18.10" @@ -1010,7 +1029,7 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/traverse@^7.1.6", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.1.6", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.7.2": version "7.20.1" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.1.tgz#9b15ccbf882f6d107eeeecf263fbcdd208777ec8" integrity sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA== @@ -1026,7 +1045,23 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": +"@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.5.tgz#78eb244bea8270fdda1ef9af22a5d5e5b7e57133" + integrity sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.5" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.20.5" + "@babel/types" "^7.20.5" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.2.tgz#67ac09266606190f496322dbaff360fdaa5e7842" integrity sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog== @@ -1035,6 +1070,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" + integrity sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -2507,9 +2551,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001400: - version "1.0.30001431" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz#e7c59bd1bc518fae03a4656be442ce6c4887a795" - integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ== + version "1.0.30001435" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001435.tgz#502c93dbd2f493bee73a408fe98e98fb1dad10b2" + integrity sha512-kdCkUTjR+v4YAJelyiDTqiu82BDr4W4CP5sgTA0ZBmqn30XfS2ZghPLMowik9TPhS+psWJiUNxsqLyurDbmutA== center-align@^0.1.1: version "0.1.3" @@ -5969,10 +6013,10 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.10: - version "0.13.10" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" - integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== regenerator-transform@^0.15.0: version "0.15.0" From 3870e3395d0caad778ac83eb5ee2daeef53f0e9e Mon Sep 17 00:00:00 2001 From: Faye Duxovni Date: Thu, 1 Dec 2022 04:49:36 -0500 Subject: [PATCH 11/60] Add method to get outgoing room key requests for a given event (#2930) * Add method to get outgoing room key requests for a given event * Write test, fix typo * Add test case for non-encrypted event --- spec/integ/matrix-client-crypto.spec.ts | 41 +++++++++++++++++++++++++ src/client.ts | 29 ++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/spec/integ/matrix-client-crypto.spec.ts b/spec/integ/matrix-client-crypto.spec.ts index 38de34aa59d..847bb8528ae 100644 --- a/spec/integ/matrix-client-crypto.spec.ts +++ b/spec/integ/matrix-client-crypto.spec.ts @@ -679,4 +679,45 @@ describe("MatrixClient crypto", () => { }); await httpBackend.flushAllExpected(); }); + + it("Checks for outgoing room key requests for a given event's session", async () => { + const eventA0 = new MatrixEvent({ + sender: "@bob:example.com", + room_id: "!someroom", + content: { + algorithm: 'm.megolm.v1.aes-sha2', + session_id: "sessionid", + sender_key: "senderkey", + }, + }); + const eventA1 = new MatrixEvent({ + sender: "@bob:example.com", + room_id: "!someroom", + content: { + algorithm: 'm.megolm.v1.aes-sha2', + session_id: "sessionid", + sender_key: "senderkey", + }, + }); + const eventB = new MatrixEvent({ + sender: "@bob:example.com", + room_id: "!someroom", + content: { + algorithm: 'm.megolm.v1.aes-sha2', + session_id: "othersessionid", + sender_key: "senderkey", + }, + }); + const nonEncryptedEvent = new MatrixEvent({ + sender: "@bob:example.com", + room_id: "!someroom", + content: {}, + }); + + aliTestClient.client.crypto?.onSyncCompleted({}); + await aliTestClient.client.cancelAndResendEventRoomKeyRequest(eventA0); + expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventA1)).not.toBeNull(); + expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventB)).toBeNull(); + expect(await aliTestClient.client.getOutgoingRoomKeyRequest(nonEncryptedEvent)).toBeNull(); + }); }); diff --git a/src/client.ts b/src/client.ts index 8b8fa18c315..42709446ecc 100644 --- a/src/client.ts +++ b/src/client.ts @@ -78,6 +78,7 @@ import { IMegolmSessionData, isCryptoAvailable, VerificationMethod, + IRoomKeyRequestBody, } from './crypto'; import { DeviceInfo, IDevice } from "./crypto/deviceinfo"; import { decodeRecoveryKey } from './crypto/recoverykey'; @@ -184,7 +185,7 @@ import { RuleId, } from "./@types/PushRules"; import { IThreepid } from "./@types/threepids"; -import { CryptoStore } from "./crypto/store/base"; +import { CryptoStore, OutgoingRoomKeyRequest } from "./crypto/store/base"; import { GroupCall, IGroupCallDataChannelOptions, @@ -2631,6 +2632,32 @@ export class MatrixClient extends TypedEventEmitter { + if (!this.crypto) { + throw new Error("End-to-End encryption disabled"); + } + const wireContent = event.getWireContent(); + const requestBody: IRoomKeyRequestBody = { + session_id: wireContent.session_id, + sender_key: wireContent.sender_key, + algorithm: wireContent.algorithm, + room_id: event.getRoomId()!, + }; + if ( + !requestBody.session_id + || !requestBody.sender_key + || !requestBody.algorithm + || !requestBody.room_id + ) return Promise.resolve(null); + return this.crypto.cryptoStore.getOutgoingRoomKeyRequest(requestBody); + } + /** * Cancel a room key request for this event if one is ongoing and resend the * request. From c0090852ad577582ea529d4bc97a783085c0652a Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 1 Dec 2022 10:45:34 -0500 Subject: [PATCH 12/60] Make GroupCall work better with widgets If the client uses a widget to join group calls, like Element Web does, then the local device could be joined to the call without GroupCall knowing. This adds a field to GroupCall that allows the client to tell GroupCall when it's using another session to join the call. --- spec/unit/webrtc/groupCall.spec.ts | 23 ++++++++++++----------- src/webrtc/groupCall.ts | 24 +++++++++++++++++++++--- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/spec/unit/webrtc/groupCall.spec.ts b/spec/unit/webrtc/groupCall.spec.ts index 047474f8e12..7085f50c12d 100644 --- a/spec/unit/webrtc/groupCall.spec.ts +++ b/spec/unit/webrtc/groupCall.spec.ts @@ -180,13 +180,13 @@ describe('Group Call', function() { room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1); groupCall = new GroupCall(mockClient, room, GroupCallType.Video, false, GroupCallIntent.Prompt); - }); - - it("does not initialize local call feed, if it already is", async () => { room.currentState.members[FAKE_USER_ID_1] = { userId: FAKE_USER_ID_1, + membership: "join", } as unknown as RoomMember; + }); + it("does not initialize local call feed, if it already is", async () => { await groupCall.initLocalCallFeed(); jest.spyOn(groupCall, "initLocalCallFeed"); await groupCall.enter(); @@ -216,10 +216,6 @@ describe('Group Call', function() { }); it("sends member state event to room on enter", async () => { - room.currentState.members[FAKE_USER_ID_1] = { - userId: FAKE_USER_ID_1, - } as unknown as RoomMember; - await groupCall.create(); try { @@ -249,10 +245,6 @@ describe('Group Call', function() { }); it("sends member state event to room on leave", async () => { - room.currentState.members[FAKE_USER_ID_1] = { - userId: FAKE_USER_ID_1, - } as unknown as RoomMember; - await groupCall.create(); await groupCall.enter(); mockSendState.mockClear(); @@ -267,6 +259,15 @@ describe('Group Call', function() { ); }); + it("includes local device in participants when entered via another session", async () => { + groupCall.enteredViaAnotherSession = true; + + const hasLocalParticipant = groupCall.participants.get( + room.getMember(mockClient.getUserId()!)!, + )?.has(mockClient.getDeviceId()!); + expect(hasLocalParticipant).toBe(true); + }); + it("starts with mic unmuted in regular calls", async () => { try { await groupCall.create(); diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index 7713d6de9a0..fde46182bc1 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -285,6 +285,21 @@ export class GroupCall extends TypedEventEmitter< this._creationTs = value; } + private _enteredViaAnotherSession = false; + + /** + * Whether the local device has entered this call via another session, such + * as a widget. + */ + public get enteredViaAnotherSession(): boolean { + return this._enteredViaAnotherSession; + } + + public set enteredViaAnotherSession(value: boolean) { + this._enteredViaAnotherSession = value; + this.updateParticipants(); + } + /** * Executes the given callback on all calls in this group call. * @param f The callback. @@ -1170,7 +1185,7 @@ export class GroupCall extends TypedEventEmitter< const participants = new Map>(); const now = Date.now(); - const entered = this.state === GroupCallState.Entered; + const entered = this.state === GroupCallState.Entered || this.enteredViaAnotherSession; let nextExpiration = Infinity; for (const e of this.getMemberStateEvents()) { @@ -1344,8 +1359,11 @@ export class GroupCall extends TypedEventEmitter< await this.updateDevices(devices => { const newDevices = devices.filter(d => { const device = deviceMap.get(d.device_id); - return device?.last_seen_ts !== undefined - && !(d.device_id === this.client.getDeviceId()! && this.state !== GroupCallState.Entered); + return device?.last_seen_ts !== undefined && !( + d.device_id === this.client.getDeviceId()! + && this.state !== GroupCallState.Entered + && !this.enteredViaAnotherSession + ); }); // Skip the update if the devices are unchanged From 9de9ff76b50c24d401c87c6bdd29a9f4e2c161ac Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 1 Dec 2022 23:27:31 -0500 Subject: [PATCH 13/60] Test cleanMemberState --- spec/test-utils/webrtc.ts | 3 + spec/unit/webrtc/groupCall.spec.ts | 166 +++++++++++++++++++++++++++-- 2 files changed, 163 insertions(+), 6 deletions(-) diff --git a/spec/test-utils/webrtc.ts b/spec/test-utils/webrtc.ts index 8d9423796a3..6a1b1a35794 100644 --- a/spec/test-utils/webrtc.ts +++ b/spec/test-utils/webrtc.ts @@ -416,6 +416,9 @@ export class MockCallMatrixClient extends TypedEventEmitter().mockReturnValue([]); public getRoom = jest.fn(); + public supportsExperimentalThreads(): boolean { return true; } + public async decryptEventIfNeeded(): Promise {} + public typed(): MatrixClient { return this as unknown as MatrixClient; } public emitRoomState(event: MatrixEvent, state: RoomState): void { diff --git a/spec/unit/webrtc/groupCall.spec.ts b/spec/unit/webrtc/groupCall.spec.ts index 7085f50c12d..3c9266b98c8 100644 --- a/spec/unit/webrtc/groupCall.spec.ts +++ b/spec/unit/webrtc/groupCall.spec.ts @@ -25,7 +25,7 @@ import { } from '../../../src'; import { RoomStateEvent } from "../../../src/models/room-state"; import { GroupCall, GroupCallEvent, GroupCallState } from "../../../src/webrtc/groupCall"; -import { MatrixClient } from "../../../src/client"; +import { IMyDevice, MatrixClient } from "../../../src/client"; import { installWebRTCMocks, MockCallFeed, @@ -260,12 +260,15 @@ describe('Group Call', function() { }); it("includes local device in participants when entered via another session", async () => { - groupCall.enteredViaAnotherSession = true; - - const hasLocalParticipant = groupCall.participants.get( + const hasLocalParticipant = () => groupCall.participants.get( room.getMember(mockClient.getUserId()!)!, - )?.has(mockClient.getDeviceId()!); - expect(hasLocalParticipant).toBe(true); + )?.has(mockClient.getDeviceId()!) ?? false; + + expect(groupCall.enteredViaAnotherSession).toBe(false); + expect(hasLocalParticipant()).toBe(false); + + groupCall.enteredViaAnotherSession = true; + expect(hasLocalParticipant()).toBe(true); }); it("starts with mic unmuted in regular calls", async () => { @@ -1271,4 +1274,155 @@ describe('Group Call', function() { }); }); }); + + describe("cleaning member state", () => { + const bobWeb: IMyDevice = { + device_id: "bobweb", + last_seen_ts: 0, + }; + const bobDesktop: IMyDevice = { + device_id: "bobdesktop", + last_seen_ts: 0, + }; + const bobDesktopOffline: IMyDevice = { + device_id: "bobdesktopoffline", + last_seen_ts: 1000 * 60 * 60 * -2, // 2 hours ago + }; + const bobDesktopNeverOnline: IMyDevice = { + device_id: "bobdesktopneveronline", + }; + + const mkContent = (devices: IMyDevice[]) => ({ + "m.calls": [{ + "m.call_id": groupCall.groupCallId, + "m.devices": devices.map(d => ({ + device_id: d.device_id, session_id: "1", feeds: [], expires_ts: 1000 * 60 * 10, + })), + }], + }); + + const expectDevices = (devices: IMyDevice[]) => expect( + room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, FAKE_USER_ID_2)?.getContent(), + ).toEqual({ + "m.calls": [{ + "m.call_id": groupCall.groupCallId, + "m.devices": devices.map(d => ({ + device_id: d.device_id, session_id: "1", feeds: [], expires_ts: expect.any(Number), + })), + }], + }); + + let mockClient: MatrixClient; + let room: Room; + let groupCall: GroupCall; + + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(0); + }); + + afterAll(() => jest.useRealTimers()); + + beforeEach(async () => { + const typedMockClient = new MockCallMatrixClient( + FAKE_USER_ID_2, bobWeb.device_id, FAKE_SESSION_ID_2, + ); + jest.spyOn(typedMockClient, "sendStateEvent").mockImplementation( + async (roomId, eventType, content, stateKey) => { + const eventId = `$${Math.random()}`; + if (roomId === room.roomId) { + room.addLiveEvents([new MatrixEvent({ + event_id: eventId, + type: eventType, + room_id: roomId, + sender: FAKE_USER_ID_2, + content, + state_key: stateKey, + })]); + } + return { event_id: eventId }; + }, + ); + mockClient = typedMockClient as unknown as MatrixClient; + + room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_2); + room.getMember = jest.fn().mockImplementation((userId) => ({ userId })); + + groupCall = new GroupCall( + mockClient, + room, + GroupCallType.Video, + false, + GroupCallIntent.Prompt, + FAKE_CONF_ID, + ); + await groupCall.create(); + + mockClient.getDevices = async () => ({ + devices: [ + bobWeb, + bobDesktop, + bobDesktopOffline, + bobDesktopNeverOnline, + ], + }); + }); + + afterEach(() => groupCall.leave()); + + it("doesn't clean up valid devices", async () => { + await groupCall.enter(); + await mockClient.sendStateEvent( + room.roomId, + EventType.GroupCallMemberPrefix, + mkContent([bobWeb, bobDesktop]), + FAKE_USER_ID_2, + ); + + await groupCall.cleanMemberState(); + expectDevices([bobWeb, bobDesktop]); + }); + + it("cleans up our own device if we're disconnected", async () => { + await mockClient.sendStateEvent( + room.roomId, + EventType.GroupCallMemberPrefix, + mkContent([bobWeb, bobDesktop]), + FAKE_USER_ID_2, + ); + + await groupCall.cleanMemberState(); + expectDevices([bobDesktop]); + }); + + it("doesn't clean up the local device if entered via another session", async () => { + groupCall.enteredViaAnotherSession = true; + await mockClient.sendStateEvent( + room.roomId, + EventType.GroupCallMemberPrefix, + mkContent([bobWeb]), + FAKE_USER_ID_2, + ); + + await groupCall.cleanMemberState(); + expectDevices([bobWeb]); + }); + + it("cleans up devices that have never been online", async () => { + await mockClient.sendStateEvent( + room.roomId, + EventType.GroupCallMemberPrefix, + mkContent([bobDesktop, bobDesktopNeverOnline]), + FAKE_USER_ID_2, + ); + + await groupCall.cleanMemberState(); + expectDevices([bobDesktop]); + }); + + it("no-ops if there are no state events", async () => { + await groupCall.cleanMemberState(); + expect(room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, FAKE_USER_ID_2)).toBe(null); + }); + }); }); From 43bfa0c0208db3e0953c38115d21e555596b88b2 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Fri, 2 Dec 2022 15:01:15 +0100 Subject: [PATCH 14/60] Switch to stable /relations endpoint, stop using unspecced original_event field (#2911) * Switch to stable /relations endpoint, stop using unspecced original_event field * Adapt the tests to the changed endpoint --- .../matrix-client-event-timeline.spec.ts | 39 +++++++++++++++---- spec/integ/matrix-client-relations.spec.ts | 20 ++++++++-- src/@types/requests.ts | 1 - src/client.ts | 23 +++++------ 4 files changed, 60 insertions(+), 23 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index fe7393bfd73..4bf36baa88c 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -611,7 +611,12 @@ describe("MatrixClient event timelines", function() { return THREAD_ROOT; }); - httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" + + httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!)) + .respond(200, function() { + return THREAD_ROOT; + }); + + httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" + encodeURIComponent(THREAD_ROOT.event_id!) + "/" + encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1") .respond(200, function() { @@ -650,7 +655,6 @@ describe("MatrixClient event timelines", function() { encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1") .respond(200, function() { return { - original_event: THREAD_ROOT, chunk: [THREAD_REPLY], // no next batch as this is the oldest end of the timeline }; @@ -660,11 +664,17 @@ describe("MatrixClient event timelines", function() { await httpBackend.flushAllExpected(); const timelineSet = thread.timelineSet; + httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!)) + .respond(200, function() { + return THREAD_ROOT; + }); + const timelinePromise = client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!); - const timeline = await timelinePromise; + const [timeline] = await Promise.all([timelinePromise, httpBackend.flushAllExpected()]); - expect(timeline!.getEvents().find(e => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy(); - expect(timeline!.getEvents().find(e => e.getId() === THREAD_REPLY.event_id!)).toBeTruthy(); + const eventIds = timeline!.getEvents().map(it => it.getId()); + expect(eventIds).toContain(THREAD_ROOT.event_id); + expect(eventIds).toContain(THREAD_REPLY.event_id); }); it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => { @@ -1504,7 +1514,22 @@ describe("MatrixClient event timelines", function() { state: [], end: "end_token", }); - httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" + + httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + + encodeURIComponent(THREAD_ROOT.event_id!)) + .respond(200, function() { + return THREAD_ROOT; + }); + httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + + encodeURIComponent(THREAD_ROOT.event_id!)) + .respond(200, function() { + return THREAD_ROOT; + }); + httpBackend.when("GET", "/rooms/!foo%3Abar/event/" + + encodeURIComponent(THREAD_ROOT.event_id!)) + .respond(200, function() { + return THREAD_ROOT; + }); + httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" + encodeURIComponent(THREAD_ROOT.event_id!) + "/" + encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Backward, "start_token")) .respond(200, function() { @@ -1513,7 +1538,7 @@ describe("MatrixClient event timelines", function() { chunk: [], }; }); - httpBackend.when("GET", "/rooms/!foo%3Abar/relations/" + + httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" + encodeURIComponent(THREAD_ROOT.event_id!) + "/" + encodeURIComponent(THREAD_RELATION_TYPE.name) + buildParams(Direction.Forward, "end_token")) .respond(200, function() { diff --git a/spec/integ/matrix-client-relations.spec.ts b/spec/integ/matrix-client-relations.spec.ts index 456db2efb07..5a4fa2bd0dc 100644 --- a/spec/integ/matrix-client-relations.spec.ts +++ b/spec/integ/matrix-client-relations.spec.ts @@ -55,7 +55,10 @@ describe("MatrixClient relations", () => { const response = client!.relations(roomId, '$event-0', null, null); httpBackend! - .when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b") + .when("GET", "/rooms/!room%3Ahere/event/%24event-0") + .respond(200, null); + httpBackend! + .when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=b") .respond(200, { chunk: [], next_batch: 'NEXT' }); await httpBackend!.flushAllExpected(); @@ -67,7 +70,10 @@ describe("MatrixClient relations", () => { const response = client!.relations(roomId, '$event-0', 'm.reference', null); httpBackend! - .when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b") + .when("GET", "/rooms/!room%3Ahere/event/%24event-0") + .respond(200, null); + httpBackend! + .when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b") .respond(200, { chunk: [], next_batch: 'NEXT' }); await httpBackend!.flushAllExpected(); @@ -78,10 +84,13 @@ describe("MatrixClient relations", () => { it("should read related events with relation type and event type", async () => { const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message'); + httpBackend! + .when("GET", "/rooms/!room%3Ahere/event/%24event-0") + .respond(200, null); httpBackend! .when( "GET", - "/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b", + "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b", ) .respond(200, { chunk: [], next_batch: 'NEXT' }); @@ -98,10 +107,13 @@ describe("MatrixClient relations", () => { to: 'TO', }); + httpBackend! + .when("GET", "/rooms/!room%3Ahere/event/%24event-0") + .respond(200, null); httpBackend! .when( "GET", - "/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO", + "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO", ) .respond(200, { chunk: [], next_batch: 'NEXT' }); diff --git a/src/@types/requests.ts b/src/@types/requests.ts index f9095455e15..936ace5b46e 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -153,7 +153,6 @@ export interface IRelationsRequestOpts { } export interface IRelationsResponse { - original_event: IEvent; chunk: IEvent[]; next_batch?: string; prev_batch?: string; diff --git a/src/client.ts b/src/client.ts index 42709446ecc..355a0af0e33 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5469,7 +5469,8 @@ export class MatrixClient extends TypedEventEmitter { const fetchedEventType = eventType ? this.getEncryptedIfNeededEventType(roomId, eventType) : null; - const result = await this.fetchRelations( - roomId, - eventId, - relationType, - fetchedEventType, - opts); + const [eventResult, result] = await Promise.all([ + this.fetchRoomEvent(roomId, eventId), + this.fetchRelations(roomId, eventId, relationType, fetchedEventType, opts), + ]); const mapper = this.getEventMapper(); - const originalEvent = result.original_event ? mapper(result.original_event) : undefined; + const originalEvent = eventResult ? mapper(eventResult) : undefined; let events = result.chunk.map(mapper); if (fetchedEventType === EventType.RoomMessageEncrypted) { @@ -7669,7 +7670,7 @@ export class MatrixClient extends TypedEventEmitter Date: Fri, 2 Dec 2022 15:01:43 +0100 Subject: [PATCH 15/60] Include pending events in thread summary and count again (#2922) * Include pending events in thread summary and count again * Pass through pending event status --- spec/unit/models/thread.spec.ts | 50 +++++++++++++++++++++++++++++++-- src/models/room.ts | 1 + src/models/thread.ts | 26 ++++++++++++++--- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/spec/unit/models/thread.spec.ts b/spec/unit/models/thread.spec.ts index 37e77954795..1ffb466370f 100644 --- a/spec/unit/models/thread.spec.ts +++ b/spec/unit/models/thread.spec.ts @@ -14,11 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from "../../../src/client"; +import { MatrixClient, PendingEventOrdering } from "../../../src/client"; import { Room } from "../../../src/models/room"; -import { Thread } from "../../../src/models/thread"; +import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/thread"; import { mkThread } from "../../test-utils/thread"; import { TestClient } from "../../TestClient"; +import { emitPromise, mkMessage } from "../../test-utils/test-utils"; +import { EventStatus } from "../../../src"; describe('Thread', () => { describe("constructor", () => { @@ -30,6 +32,50 @@ describe('Thread', () => { }); }); + it("includes pending events in replyCount", async () => { + const myUserId = "@bob:example.org"; + const testClient = new TestClient( + myUserId, + "DEVICE", + "ACCESS_TOKEN", + undefined, + { timelineSupport: false }, + ); + const client = testClient.client; + const room = new Room("123", client, myUserId, { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + jest.spyOn(client, "getRoom").mockReturnValue(room); + + const { thread } = mkThread({ + room, + client, + authorId: myUserId, + participantUserIds: ["@alice:example.org"], + length: 3, + }); + await emitPromise(thread, ThreadEvent.Update); + expect(thread.length).toBe(2); + + const event = mkMessage({ + room: room.roomId, + user: myUserId, + msg: "thread reply", + relatesTo: { + rel_type: THREAD_RELATION_TYPE.name, + event_id: thread.id, + }, + event: true, + }); + await thread.processEvent(event); + event.setStatus(EventStatus.SENDING); + room.addPendingEvent(event, "txn01"); + + await emitPromise(thread, ThreadEvent.Update); + expect(thread.length).toBe(3); + }); + describe("hasUserReadEvent", () => { const myUserId = "@bob:example.org"; let client: MatrixClient; diff --git a/src/models/room.ts b/src/models/room.ts index de3d18733de..7c01fca5ca7 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -2017,6 +2017,7 @@ export class Room extends ReadReceipt { const thread = new Thread(threadId, rootEvent, { room: this, client: this.client, + pendingEventOrdering: this.opts.pendingEventOrdering, }); // This is necessary to be able to jump to events in threads: diff --git a/src/models/thread.ts b/src/models/thread.ts index 8c9826c35ec..d641ba8ec3b 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -16,10 +16,10 @@ limitations under the License. import { Optional } from "matrix-events-sdk"; -import { MatrixClient } from "../client"; +import { MatrixClient, PendingEventOrdering } from "../client"; import { TypedReEmitter } from "../ReEmitter"; import { RelationType } from "../@types/event"; -import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "./event"; +import { EventStatus, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "./event"; import { EventTimeline } from "./event-timeline"; import { EventTimelineSet, EventTimelineSetHandlerMap } from './event-timeline-set'; import { NotificationCountType, Room, RoomEvent } from './room'; @@ -51,6 +51,7 @@ export type EventHandlerMap = { interface IThreadOpts { room: Room; client: MatrixClient; + pendingEventOrdering?: PendingEventOrdering; } export enum FeatureSupport { @@ -88,9 +89,12 @@ export class Thread extends ReadReceipt { private lastEvent: MatrixEvent | undefined; private replyCount = 0; + private lastPendingEvent: MatrixEvent | undefined; + private pendingReplyCount = 0; public readonly room: Room; public readonly client: MatrixClient; + private readonly pendingEventOrdering: PendingEventOrdering; public initialEventsFetched = !Thread.hasServerSideSupport; @@ -109,6 +113,7 @@ export class Thread extends ReadReceipt { this.room = opts.room; this.client = opts.client; + this.pendingEventOrdering = opts.pendingEventOrdering ?? PendingEventOrdering.Chronological; this.timelineSet = new EventTimelineSet(this.room, { timelineSupport: true, pendingEvents: true, @@ -300,6 +305,19 @@ export class Thread extends ReadReceipt { bundledRelationship = this.getRootEventBundledRelationship(); } + let pendingEvents: MatrixEvent[]; + if (this.pendingEventOrdering === PendingEventOrdering.Detached) { + pendingEvents = this.room.getPendingEvents() + .filter(ev => ev.isRelation(THREAD_RELATION_TYPE.name) && this.id === ev.threadRootId); + await Promise.all(pendingEvents.map(ev => this.processEvent(ev))); + } else { + pendingEvents = this.events + .filter(ev => ev.isRelation(THREAD_RELATION_TYPE.name)) + .filter(ev => ev.status !== EventStatus.SENT && ev.status !== EventStatus.CANCELLED); + } + this.lastPendingEvent = pendingEvents.length ? pendingEvents[pendingEvents.length - 1] : undefined; + this.pendingReplyCount = pendingEvents.length; + if (Thread.hasServerSideSupport && bundledRelationship) { this.replyCount = bundledRelationship.count; this._currentUserParticipated = !!bundledRelationship.current_user_participated; @@ -393,14 +411,14 @@ export class Thread extends ReadReceipt { * exclude annotations from that number */ public get length(): number { - return this.replyCount; + return this.replyCount + this.pendingReplyCount; } /** * A getter for the last event added to the thread, if known. */ public get replyToEvent(): Optional { - return this.lastEvent ?? this.lastReply(); + return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply(); } public get events(): MatrixEvent[] { From fa2eeac5b827bac79af8e1ccb4763a856118231d Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Fri, 2 Dec 2022 15:50:19 +0100 Subject: [PATCH 16/60] Improve perceived performance for threads (#2901) * Improve perceived performance for threads * Improve method naming and make it private --- src/models/thread.ts | 45 +++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/models/thread.ts b/src/models/thread.ts index d641ba8ec3b..93b6e046024 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -132,7 +132,7 @@ export class Thread extends ReadReceipt { // even if this thread is thought to be originating from this client, we initialise it as we may be in a // gappy sync and a thread around this event may already exist. - this.initialiseThread(); + this.updateThreadMetadata(); this.setEventMetadata(this.rootEvent); } @@ -193,7 +193,7 @@ export class Thread extends ReadReceipt { this._currentUserParticipated = false; this.emit(ThreadEvent.Delete, this); } else { - await this.initialiseThread(); + await this.updateThreadMetadata(); } }; @@ -214,7 +214,7 @@ export class Thread extends ReadReceipt { if (this.lastEvent === event) return; if (!event.isRelation(THREAD_RELATION_TYPE.name)) return; - await this.initialiseThread(); + await this.updateThreadMetadata(); this.emit(ThreadEvent.NewReply, this, event); }; @@ -238,7 +238,7 @@ export class Thread extends ReadReceipt { public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void { events.forEach(ev => this.addEvent(ev, toStartOfTimeline, false)); - this.initialiseThread(); + this.updateThreadMetadata(); } /** @@ -283,7 +283,7 @@ export class Thread extends ReadReceipt { if (emit) { this.emit(ThreadEvent.NewReply, this, event); - this.initialiseThread(); + this.updateThreadMetadata(); } } @@ -298,11 +298,19 @@ export class Thread extends ReadReceipt { return rootEvent?.getServerAggregatedRelation(THREAD_RELATION_TYPE.name); } - public async initialiseThread(): Promise { - let bundledRelationship = this.getRootEventBundledRelationship(); - if (Thread.hasServerSideSupport) { - await this.fetchRootEvent(); - bundledRelationship = this.getRootEventBundledRelationship(); + private async processRootEvent(): Promise { + const bundledRelationship = this.getRootEventBundledRelationship(); + if (Thread.hasServerSideSupport && bundledRelationship) { + this.replyCount = bundledRelationship.count; + this._currentUserParticipated = !!bundledRelationship.current_user_participated; + + const mapper = this.client.getEventMapper(); + // re-insert roomId + this.lastEvent = mapper({ + ...bundledRelationship.latest_event, + room_id: this.roomId, + }); + await this.processEvent(this.lastEvent); } let pendingEvents: MatrixEvent[]; @@ -317,15 +325,18 @@ export class Thread extends ReadReceipt { } this.lastPendingEvent = pendingEvents.length ? pendingEvents[pendingEvents.length - 1] : undefined; this.pendingReplyCount = pendingEvents.length; + } - if (Thread.hasServerSideSupport && bundledRelationship) { - this.replyCount = bundledRelationship.count; - this._currentUserParticipated = !!bundledRelationship.current_user_participated; - - const mapper = this.client.getEventMapper(); - this.lastEvent = mapper(bundledRelationship.latest_event); - await this.processEvent(this.lastEvent); + private async updateThreadMetadata(): Promise { + if (Thread.hasServerSideSupport) { + // Ensure we show *something* as soon as possible, we'll update it as soon as we get better data, but we + // don't want the thread preview to be empty if we can avoid it + if (!this.initialEventsFetched) { + await this.processRootEvent(); + } + await this.fetchRootEvent(); } + await this.processRootEvent(); if (!this.initialEventsFetched) { this.initialEventsFetched = true; From 53a45a34dffc500a21af7774c1a9217f5733cf0f Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 2 Dec 2022 15:41:15 +0000 Subject: [PATCH 17/60] Fix highlight notifications increasing when total notification is zero (#2937) --- spec/unit/notifications.spec.ts | 4 ++-- src/client.ts | 3 +-- src/models/room.ts | 2 +- src/models/thread.ts | 13 ++++++------- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/spec/unit/notifications.spec.ts b/spec/unit/notifications.spec.ts index d7936014b7d..e7e637d4acc 100644 --- a/spec/unit/notifications.spec.ts +++ b/spec/unit/notifications.spec.ts @@ -114,8 +114,8 @@ describe("fixNotificationCountOnDecryption", () => { fixNotificationCountOnDecryption(mockClient, event); - expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0); - expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0); + expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(1); + expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1); }); it("changes the thread count to highlight on decryption", () => { diff --git a/src/client.ts b/src/client.ts index 355a0af0e33..9abcd967f70 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9379,7 +9379,6 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri const isThreadEvent = !!event.threadRootId && !event.isThreadRoot; - const totalCount = room.getUnreadCountForEventContext(NotificationCountType.Total, event); const currentCount = room.getUnreadCountForEventContext(NotificationCountType.Highlight, event); // Ensure the unread counts are kept up to date if the event is encrypted @@ -9387,7 +9386,7 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri // have encrypted events to avoid other code from resetting 'highlight' to zero. const oldHighlight = !!oldActions?.tweaks?.highlight; const newHighlight = !!actions?.tweaks?.highlight; - if ((oldHighlight !== newHighlight || currentCount > 0) && totalCount > 0) { + if (oldHighlight !== newHighlight || currentCount > 0) { // TODO: Handle mentions received while the client is offline // See also https://github.com/vector-im/element-web/issues/9069 const hasReadEvent = isThreadEvent diff --git a/src/models/room.ts b/src/models/room.ts index 7c01fca5ca7..ceac37135c8 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -1696,7 +1696,7 @@ export class Room extends ReadReceipt { Array.from(this.threads) .forEach(([, thread]) => { if (thread.length === 0) return; - const currentUserParticipated = thread.events.some(event => { + const currentUserParticipated = thread.timeline.some(event => { return event.getSender() === this.client.getUserId(); }); if (filterType !== ThreadFilterType.My || currentUserParticipated) { diff --git a/src/models/thread.ts b/src/models/thread.ts index 93b6e046024..c595995b1cc 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -82,6 +82,7 @@ export class Thread extends ReadReceipt { * A reference to all the events ID at the bottom of the threads */ public readonly timelineSet: EventTimelineSet; + public timeline: MatrixEvent[] = []; private _currentUserParticipated = false; @@ -186,7 +187,7 @@ export class Thread extends ReadReceipt { private onRedaction = async (event: MatrixEvent): Promise => { if (event.threadRootId !== this.id) return; // ignore redactions for other timelines if (this.replyCount <= 0) { - for (const threadEvent of this.events) { + for (const threadEvent of this.timeline) { this.clearEventMetadata(threadEvent); } this.lastEvent = this.rootEvent; @@ -233,6 +234,7 @@ export class Thread extends ReadReceipt { roomState: this.roomState, }, ); + this.timeline = this.events; } } @@ -292,6 +294,7 @@ export class Thread extends ReadReceipt { this.setEventMetadata(event); await this.fetchEditsWhereNeeded(event); } + this.timeline = this.events; } private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship | undefined { @@ -403,8 +406,8 @@ export class Thread extends ReadReceipt { * Return last reply to the thread, if known. */ public lastReply(matches: (ev: MatrixEvent) => boolean = (): boolean => true): MatrixEvent | null { - for (let i = this.events.length - 1; i >= 0; i--) { - const event = this.events[i]; + for (let i = this.timeline.length - 1; i >= 0; i--) { + const event = this.timeline[i]; if (matches(event)) { return event; } @@ -452,10 +455,6 @@ export class Thread extends ReadReceipt { return this.timelineSet; } - public get timeline(): MatrixEvent[] { - return this.events; - } - public addReceipt(event: MatrixEvent, synthetic: boolean): void { throw new Error("Unsupported function on the thread model"); } From 8a7fd270e473329ed1fc4619a92cff557a0486c1 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Fri, 2 Dec 2022 17:11:18 +0100 Subject: [PATCH 18/60] Move updated threads to the end of the thread list (#2923) * Move updated threads to the end of the thread list * Write new tests --- .../matrix-client-event-timeline.spec.ts | 154 +++++++++++++++++- src/models/room.ts | 14 +- 2 files changed, 160 insertions(+), 8 deletions(-) diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 4bf36baa88c..663af8fbfcd 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -27,11 +27,13 @@ import { MatrixEvent, PendingEventOrdering, Room, + RoomEvent, } from "../../src/matrix"; import { logger } from "../../src/logger"; import { encodeUri } from "../../src/utils"; import { TestClient } from "../TestClient"; import { FeatureSupport, Thread, THREAD_RELATION_TYPE } from "../../src/models/thread"; +import { emitPromise } from "../test-utils/test-utils"; const userId = "@alice:localhost"; const userName = "Alice"; @@ -1093,10 +1095,27 @@ describe("MatrixClient event timelines", function() { return request; } - function respondToContext(): ExpectedHttpRequest { + function respondToThread( + root: Partial, + replies: Partial[], + ): ExpectedHttpRequest { + const request = httpBackend.when("GET", "/_matrix/client/v1/rooms/!foo%3Abar/relations/" + + encodeURIComponent(root.event_id!) + "/" + + encodeURIComponent(THREAD_RELATION_TYPE.name) + "?dir=b&limit=1"); + request.respond(200, function() { + return { + original_event: root, + chunk: [replies], + // no next batch as this is the oldest end of the timeline + }; + }); + return request; + } + + function respondToContext(event: Partial = THREAD_ROOT): ExpectedHttpRequest { const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/context/$eventId", { $roomId: roomId, - $eventId: THREAD_ROOT.event_id!, + $eventId: event.event_id!, })); request.respond(200, { end: `${Direction.Forward}${RANDOM_TOKEN}1`, @@ -1104,10 +1123,18 @@ describe("MatrixClient event timelines", function() { state: [], events_before: [], events_after: [], - event: THREAD_ROOT, + event: event, }); return request; } + function respondToEvent(event: Partial = THREAD_ROOT): ExpectedHttpRequest { + const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/event/$eventId", { + $roomId: roomId, + $eventId: event.event_id!, + })); + request.respond(200, event); + return request; + } function respondToMessagesRequest(): ExpectedHttpRequest { const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/messages", { $roomId: roomId, @@ -1193,6 +1220,127 @@ describe("MatrixClient event timelines", function() { expect(myThreads.getPendingEvents()).toHaveLength(0); expect(room.getPendingEvents()).toHaveLength(1); }); + + it("should handle thread updates by reordering the thread list", async () => { + // Test data for a second thread + const THREAD2_ROOT = utils.mkEvent({ + room: roomId, + user: userId, + type: "m.room.message", + content: { + "body": "thread root", + "msgtype": "m.text", + }, + unsigned: { + "m.relations": { + "io.element.thread": { + //"latest_event": undefined, + "count": 1, + "current_user_participated": true, + }, + }, + }, + event: false, + }); + + const THREAD2_REPLY = utils.mkEvent({ + room: roomId, + user: userId, + type: "m.room.message", + content: { + "body": "thread reply", + "msgtype": "m.text", + "m.relates_to": { + // We can't use the const here because we change server support mode for test + rel_type: "io.element.thread", + event_id: THREAD_ROOT.event_id, + }, + }, + event: false, + }); + + // @ts-ignore we know this is a defined path for THREAD ROOT + THREAD2_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD2_REPLY; + + // Test data for a second reply to the first thread + const THREAD_REPLY2 = utils.mkEvent({ + room: roomId, + user: userId, + type: "m.room.message", + content: { + "body": "thread reply", + "msgtype": "m.text", + "m.relates_to": { + // We can't use the const here because we change server support mode for test + rel_type: "io.element.thread", + event_id: THREAD_ROOT.event_id, + }, + }, + event: false, + }); + + // Test data for the first thread, with the second reply + const THREAD_ROOT_UPDATED = { + ...THREAD_ROOT, + unsigned: { + ...THREAD_ROOT.unsigned, + "m.relations": { + ...THREAD_ROOT.unsigned!["m.relations"], + "io.element.thread": { + ...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"], + count: 2, + latest_event: THREAD_REPLY2, + }, + }, + }, + }; + + // Response with test data for the thread list request + const threadsResponse = { + chunk: [THREAD2_ROOT, THREAD_ROOT], + state: [], + next_batch: RANDOM_TOKEN as string | null, + }; + + // @ts-ignore + client.clientOpts.experimentalThreadSupport = true; + Thread.setServerSideSupport(FeatureSupport.Stable); + Thread.setServerSideListSupport(FeatureSupport.Stable); + Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable); + + await client.stopClient(); // we don't need the client to be syncing at this time + const room = client.getRoom(roomId)!; + + // Setup room threads + const timelineSets = await room!.createThreadsTimelineSets(); + expect(timelineSets).not.toBeNull(); + respondToThreads(threadsResponse); + respondToThreads(threadsResponse); + respondToEvent(THREAD_ROOT); + respondToEvent(THREAD_ROOT); + respondToEvent(THREAD2_ROOT); + respondToEvent(THREAD2_ROOT); + respondToThread(THREAD_ROOT, [THREAD_REPLY]); + respondToThread(THREAD2_ROOT, [THREAD2_REPLY]); + await flushHttp(room.fetchRoomThreads()); + const [allThreads] = timelineSets!; + const timeline = allThreads.getLiveTimeline()!; + // Test threads are in chronological order + expect(timeline.getEvents().map(it => it.event.event_id)) + .toEqual([THREAD_ROOT.event_id, THREAD2_ROOT.event_id]); + + // Test adding a second event to the first thread + const thread = room.getThread(THREAD_ROOT.event_id!)!; + const prom = emitPromise(allThreads!, RoomEvent.Timeline); + await thread.addEvent(client.getEventMapper()(THREAD_REPLY2), false); + respondToEvent(THREAD_ROOT_UPDATED); + respondToEvent(THREAD_ROOT_UPDATED); + await httpBackend.flushAllExpected(); + await prom; + // Test threads are in chronological order + expect(timeline!.getEvents().map(it => it.event.event_id)) + .toEqual([THREAD2_ROOT.event_id, THREAD_ROOT.event_id]); + }); }); describe("without server compatibility", function() { diff --git a/src/models/room.ts b/src/models/room.ts index ceac37135c8..3952ed7d9cb 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -1843,7 +1843,7 @@ export class Room extends ReadReceipt { } private onThreadNewReply(thread: Thread): void { - this.updateThreadRootEvents(thread, false); + this.updateThreadRootEvents(thread, false, true); } private onThreadDelete(thread: Thread): void { @@ -1968,11 +1968,11 @@ export class Room extends ReadReceipt { )); } - private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean): void => { + private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean, recreateEvent: boolean): void => { if (thread.length) { - this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline); + this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline, recreateEvent); if (thread.hasCurrentUserParticipated) { - this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline); + this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline, recreateEvent); } } }; @@ -1981,8 +1981,12 @@ export class Room extends ReadReceipt { timelineSet: Optional, thread: Thread, toStartOfTimeline: boolean, + recreateEvent: boolean, ): void => { if (timelineSet && thread.rootEvent) { + if (recreateEvent) { + timelineSet.removeEvent(thread.id); + } if (Thread.hasServerSideSupport) { timelineSet.addLiveEvent(thread.rootEvent, { duplicateStrategy: DuplicateStrategy.Replace, @@ -2046,7 +2050,7 @@ export class Room extends ReadReceipt { } if (this.threadsReady) { - this.updateThreadRootEvents(thread, toStartOfTimeline); + this.updateThreadRootEvents(thread, toStartOfTimeline, false); } this.emit(ThreadEvent.New, thread, toStartOfTimeline); From 779980476273a7356131dc63dd3fd4bbdf420523 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 2 Dec 2022 16:27:59 +0000 Subject: [PATCH 19/60] Move @types deps into devDeps (#2927) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 969c71cba4a..5cfc418633d 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ ], "dependencies": { "@babel/runtime": "^7.12.5", - "@types/sdp-transform": "^2.4.5", "another-json": "^0.2.0", "bs58": "^5.0.0", "content-type": "^1.0.4", @@ -86,6 +85,7 @@ "@types/domexception": "^4.0.0", "@types/jest": "^29.0.0", "@types/node": "18", + "@types/sdp-transform": "^2.4.5", "@typescript-eslint/eslint-plugin": "^5.6.0", "@typescript-eslint/parser": "^5.6.0", "allchange": "^1.0.6", From 11d8f562c5776ef694690428169330a338d98d38 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Mon, 5 Dec 2022 14:31:58 +0100 Subject: [PATCH 20/60] Redo key sharing after own device verification (#2921) --- .../setDeviceVerification.spec.ts | 60 +++++++++++++++++++ src/crypto/index.ts | 3 + 2 files changed, 63 insertions(+) create mode 100644 spec/unit/crypto/verification/setDeviceVerification.spec.ts diff --git a/spec/unit/crypto/verification/setDeviceVerification.spec.ts b/spec/unit/crypto/verification/setDeviceVerification.spec.ts new file mode 100644 index 00000000000..3d64153ab59 --- /dev/null +++ b/spec/unit/crypto/verification/setDeviceVerification.spec.ts @@ -0,0 +1,60 @@ +/* +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 '../../../olm-loader'; + +import { CRYPTO_ENABLED, MatrixClient } from "../../../../src/client"; +import { TestClient } from "../../../TestClient"; + +const Olm = global.Olm; + +describe("crypto.setDeviceVerification", () => { + const userId = "@alice:example.com"; + const deviceId1 = "device1"; + let client: MatrixClient; + + if (!CRYPTO_ENABLED) { + return; + } + + beforeAll(async () => { + await Olm.init(); + }); + + beforeEach(async () => { + client = (new TestClient(userId, deviceId1)).client; + await client.initCrypto(); + }); + + it("client should provide crypto", () => { + expect(client.crypto).not.toBeUndefined(); + }); + + describe("when setting an own device as verified", () => { + beforeEach(async () => { + jest.spyOn(client.crypto!, "cancelAndResendAllOutgoingKeyRequests"); + await client.crypto!.setDeviceVerification( + userId, + deviceId1, + true, + ); + }); + + it("cancelAndResendAllOutgoingKeyRequests should be called", () => { + expect(client.crypto!.cancelAndResendAllOutgoingKeyRequests).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 0f203bc25cc..2d6995a58bb 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -2270,6 +2270,9 @@ export class Crypto extends TypedEventEmitter Date: Mon, 5 Dec 2022 17:00:06 +0000 Subject: [PATCH 21/60] bugfix: sliding sync initial room timelines shouldn't notify (#2933) * bugfix: sliding sync initial room timelines shouldn't notify Flag timeline events as `fromCache` when `initial: true` rooms are received. This stops notifications appearing inappropriately when you scroll the room list or spider the room list, as it causes `liveEvent=false`. * Use num_live to detect liveness; with jest test * Linting * jsdoc Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- spec/integ/sliding-sync-sdk.spec.ts | 42 ++++++++++++++++++++++++++++- src/sliding-sync-sdk.ts | 28 ++++++++++++++++--- src/sliding-sync.ts | 1 + 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 72a7eeaa2e9..7a37f572bba 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -23,7 +23,7 @@ import { TestClient } from "../TestClient"; import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator"; import { MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError, - EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent, + EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent, RoomEvent, Room, IRoomTimelineData, } from "../../src"; import { SlidingSyncSdk } from "../../src/sliding-sync-sdk"; import { SyncState } from "../../src/sync"; @@ -170,6 +170,7 @@ describe("SlidingSyncSdk", () => { const roomE = "!e_with_invite:localhost"; const roomF = "!f_calc_room_name:localhost"; const roomG = "!g_join_invite_counts:localhost"; + const roomH = "!g_num_live:localhost"; const data: Record = { [roomA]: { name: "A", @@ -275,6 +276,18 @@ describe("SlidingSyncSdk", () => { invited_count: 2, initial: true, }, + [roomH]: { + name: "H", + required_state: [], + timeline: [ + mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""), + mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId), + mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""), + mkOwnEvent(EventType.RoomMessage, { body: "live event" }), + ], + initial: true, + num_live: 1, + }, }; it("can be created with required_state and timeline", () => { @@ -326,6 +339,33 @@ describe("SlidingSyncSdk", () => { expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count); }); + it("can be created with live events", () => { + let seenLiveEvent = false; + const listener = ( + ev: MatrixEvent, + room?: Room, + toStartOfTimeline?: boolean, + deleted?: boolean, + timelineData?: IRoomTimelineData, + ) => { + if (timelineData?.liveEvent) { + assertTimelineEvents([ev], data[roomH].timeline.slice(-1)); + seenLiveEvent = true; + } + }; + client!.on(RoomEvent.Timeline, listener); + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]); + client!.off(RoomEvent.Timeline, listener); + const gotRoom = client!.getRoom(roomH); + expect(gotRoom).toBeDefined(); + if (gotRoom == null) { return; } + expect(gotRoom.name).toEqual(data[roomH].name); + expect(gotRoom.getMyMembership()).toEqual("join"); + // check the entire timeline is correct + assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), data[roomH].timeline); + expect(seenLiveEvent).toBe(true); + }); + it("can be created with invite_state", () => { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]); const gotRoom = client!.getRoom(roomE); diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 03a17bed327..c99f61f13b9 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -681,7 +681,7 @@ export class SlidingSyncSdk { } } */ - this.injectRoomEvents(room, stateEvents, timelineEvents, false); + this.injectRoomEvents(room, stateEvents, timelineEvents, roomData.num_live); // we deliberately don't add ephemeral events to the timeline room.addEphemeralEvents(ephemeralEvents); @@ -725,17 +725,19 @@ export class SlidingSyncSdk { * @param {MatrixEvent[]} stateEventList A list of state events. This is the state * at the *START* of the timeline list if it is supplied. * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index - * @param {boolean} fromCache whether the sync response came from cache * is earlier in time. Higher index is later. + * @param {number} numLive the number of events in timelineEventList which just happened, + * supplied from the server. */ public injectRoomEvents( room: Room, stateEventList: MatrixEvent[], timelineEventList?: MatrixEvent[], - fromCache = false, + numLive?: number, ): void { timelineEventList = timelineEventList || []; stateEventList = stateEventList || []; + numLive = numLive || 0; // If there are no events in the timeline yet, initialise it with // the given state events @@ -774,13 +776,31 @@ export class SlidingSyncSdk { room.currentState.setStateEvents(stateEventList); } + // the timeline is broken into 'live' events which just happened and normal timeline events + // which are still to be appended to the end of the live timeline but happened a while ago. + // The live events are marked as fromCache=false to ensure that downstream components know + // this is a live event, not historical (from a remote server cache). + + let liveTimelineEvents: MatrixEvent[] = []; + if (numLive > 0) { + // last numLive events are live + liveTimelineEvents = timelineEventList.slice(-1 * numLive); + // everything else is not live + timelineEventList = timelineEventList.slice(0, -1 * liveTimelineEvents.length); + } + // execute the timeline events. This will continue to diverge the current state // if the timeline has any state events in it. // This also needs to be done before running push rules on the events as they need // to be decorated with sender etc. room.addLiveEvents(timelineEventList, { - fromCache: fromCache, + fromCache: true, }); + if (liveTimelineEvents.length > 0) { + room.addLiveEvents(liveTimelineEvents, { + fromCache: false, + }); + } room.recalculate(); diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 171bf55c32d..cda2737d984 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -95,6 +95,7 @@ export interface MSC3575RoomData { limited?: boolean; is_dm?: boolean; prev_batch?: string; + num_live?: number; } interface ListResponse { From 2c8eece5ca5333c6e6a14e8ed53f359ed0e9e9bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 5 Dec 2022 18:44:13 +0100 Subject: [PATCH 22/60] Don't expose `calls` on `GroupCall` (#2941) --- spec/test-utils/webrtc.ts | 51 +++++++++++++- spec/unit/webrtc/call.spec.ts | 4 +- spec/unit/webrtc/callFeed.spec.ts | 55 ++++++++++----- spec/unit/webrtc/groupCall.spec.ts | 106 ++++++++++++----------------- src/webrtc/call.ts | 52 +++++++------- src/webrtc/callFeed.ts | 34 +++++++++ src/webrtc/groupCall.ts | 2 +- 7 files changed, 197 insertions(+), 107 deletions(-) diff --git a/spec/test-utils/webrtc.ts b/spec/test-utils/webrtc.ts index 6a1b1a35794..c2f1a0b25a3 100644 --- a/spec/test-utils/webrtc.ts +++ b/spec/test-utils/webrtc.ts @@ -26,6 +26,7 @@ import { MatrixClient, MatrixEvent, Room, + RoomMember, RoomState, RoomStateEvent, RoomStateEventHandlerMap, @@ -33,7 +34,7 @@ import { import { TypedEventEmitter } from "../../src/models/typed-event-emitter"; import { ReEmitter } from "../../src/ReEmitter"; import { SyncState } from "../../src/sync"; -import { CallEvent, CallEventHandlerMap, MatrixCall } from "../../src/webrtc/call"; +import { CallEvent, CallEventHandlerMap, CallState, MatrixCall } from "../../src/webrtc/call"; import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler"; import { CallFeed } from "../../src/webrtc/callFeed"; import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall"; @@ -83,6 +84,17 @@ export const DUMMY_SDP = ( export const USERMEDIA_STREAM_ID = "mock_stream_from_media_handler"; export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler"; +export const FAKE_ROOM_ID = "!fake:test.dummy"; +export const FAKE_CONF_ID = "fakegroupcallid"; + +export const FAKE_USER_ID_1 = "@alice:test.dummy"; +export const FAKE_DEVICE_ID_1 = "@AAAAAA"; +export const FAKE_SESSION_ID_1 = "alice1"; +export const FAKE_USER_ID_2 = "@bob:test.dummy"; +export const FAKE_DEVICE_ID_2 = "@BBBBBB"; +export const FAKE_SESSION_ID_2 = "bob1"; +export const FAKE_USER_ID_3 = "@charlie:test.dummy"; + class MockMediaStreamAudioSourceNode { public connect() {} } @@ -431,6 +443,43 @@ export class MockCallMatrixClient extends TypedEventEmitter { + constructor(public roomId: string, public groupCallId?: string) { + super(); + } + + public state = CallState.Ringing; + public opponentUserId = FAKE_USER_ID_1; + public opponentDeviceId = FAKE_DEVICE_ID_1; + public opponentMember = { userId: this.opponentUserId }; + public callId = "1"; + public localUsermediaFeed = { + setAudioVideoMuted: jest.fn(), + stream: new MockMediaStream("stream"), + }; + public remoteUsermediaFeed?: CallFeed; + public remoteScreensharingFeed?: CallFeed; + + public reject = jest.fn(); + public answerWithCallFeeds = jest.fn(); + public hangup = jest.fn(); + + public sendMetadataUpdate = jest.fn(); + + public on = jest.fn(); + public removeListener = jest.fn(); + + public getOpponentMember(): Partial { + return this.opponentMember; + } + + public getOpponentDeviceId(): string | undefined { + return this.opponentDeviceId; + } + + public typed(): MatrixCall { return this as unknown as MatrixCall; } +} + export class MockCallFeed { constructor( public userId: string, diff --git a/spec/unit/webrtc/call.spec.ts b/spec/unit/webrtc/call.spec.ts index ee52a518a36..d1cb5d29616 100644 --- a/spec/unit/webrtc/call.spec.ts +++ b/spec/unit/webrtc/call.spec.ts @@ -1392,7 +1392,7 @@ describe('Call', function() { it("ends call on onHangupReceived() if state is ringing", async () => { expect(call.callHasEnded()).toBe(false); - call.state = CallState.Ringing; + (call as any).state = CallState.Ringing; call.onHangupReceived({} as MCallHangupReject); expect(call.callHasEnded()).toBe(true); @@ -1424,7 +1424,7 @@ describe('Call', function() { )("ends call on onRejectReceived() if in correct state (state=%s)", async (state: CallState) => { expect(call.callHasEnded()).toBe(false); - call.state = state; + (call as any).state = state; call.onRejectReceived({} as MCallHangupReject); expect(call.callHasEnded()).toBe( diff --git a/spec/unit/webrtc/callFeed.spec.ts b/spec/unit/webrtc/callFeed.spec.ts index 635fa14fd8f..e14a1a0c56b 100644 --- a/spec/unit/webrtc/callFeed.spec.ts +++ b/spec/unit/webrtc/callFeed.spec.ts @@ -17,13 +17,30 @@ limitations under the License. import { SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes"; import { CallFeed } from "../../../src/webrtc/callFeed"; import { TestClient } from "../../TestClient"; -import { MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc"; +import { MockMatrixCall, MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc"; +import { CallEvent, CallState } from "../../../src/webrtc/call"; describe("CallFeed", () => { - let client; + const roomId = "room1"; + let client: TestClient; + let call: MockMatrixCall; + let feed: CallFeed; beforeEach(() => { client = new TestClient("@alice:foo", "somedevice", "token", undefined, {}); + call = new MockMatrixCall(roomId); + + feed = new CallFeed({ + client: client.client, + call: call.typed(), + roomId, + userId: "user1", + // @ts-ignore Mock + stream: new MockMediaStream("stream1"), + purpose: SDPStreamMetadataPurpose.Usermedia, + audioMuted: false, + videoMuted: false, + }); }); afterEach(() => { @@ -31,21 +48,6 @@ describe("CallFeed", () => { }); describe("muting", () => { - let feed: CallFeed; - - beforeEach(() => { - feed = new CallFeed({ - client, - roomId: "room1", - userId: "user1", - // @ts-ignore Mock - stream: new MockMediaStream("stream1"), - purpose: SDPStreamMetadataPurpose.Usermedia, - audioMuted: false, - videoMuted: false, - }); - }); - describe("muting by default", () => { it("should mute audio by default", () => { expect(feed.isAudioMuted()).toBeTruthy(); @@ -86,4 +88,23 @@ describe("CallFeed", () => { }); }); }); + + describe("connected", () => { + it.each([true, false])("should always be connected, if isLocal()", (val: boolean) => { + // @ts-ignore + feed._connected = val; + jest.spyOn(feed, "isLocal").mockReturnValue(true); + + expect(feed.connected).toBeTruthy(); + }); + + it.each([ + [CallState.Connected, true], + [CallState.Connecting, false], + ])("should react to call state, when !isLocal()", (state: CallState, expected: Boolean) => { + call.emit(CallEvent.State, state); + + expect(feed.connected).toBe(expected); + }); + }); }); diff --git a/spec/unit/webrtc/groupCall.spec.ts b/spec/unit/webrtc/groupCall.spec.ts index 3c9266b98c8..ad77e15bdc7 100644 --- a/spec/unit/webrtc/groupCall.spec.ts +++ b/spec/unit/webrtc/groupCall.spec.ts @@ -33,6 +33,16 @@ import { MockMediaStream, MockMediaStreamTrack, MockRTCPeerConnection, + MockMatrixCall, + FAKE_ROOM_ID, + FAKE_USER_ID_1, + FAKE_CONF_ID, + FAKE_DEVICE_ID_2, + FAKE_SESSION_ID_2, + FAKE_USER_ID_2, + FAKE_DEVICE_ID_1, + FAKE_SESSION_ID_1, + FAKE_USER_ID_3, } from '../../test-utils/webrtc'; import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes"; import { sleep } from "../../../src/utils"; @@ -41,16 +51,6 @@ import { CallFeed } from '../../../src/webrtc/callFeed'; import { CallEvent, CallState } from '../../../src/webrtc/call'; import { flushPromises } from '../../test-utils/flushPromises'; -const FAKE_ROOM_ID = "!fake:test.dummy"; -const FAKE_CONF_ID = "fakegroupcallid"; - -const FAKE_USER_ID_1 = "@alice:test.dummy"; -const FAKE_DEVICE_ID_1 = "@AAAAAA"; -const FAKE_SESSION_ID_1 = "alice1"; -const FAKE_USER_ID_2 = "@bob:test.dummy"; -const FAKE_DEVICE_ID_2 = "@BBBBBB"; -const FAKE_SESSION_ID_2 = "bob1"; -const FAKE_USER_ID_3 = "@charlie:test.dummy"; const FAKE_STATE_EVENTS = [ { getContent: () => ({ @@ -123,42 +123,6 @@ const createAndEnterGroupCall = async (cli: MatrixClient, room: Room): Promise(), - stream: new MockMediaStream("stream"), - }; - public remoteUsermediaFeed?: CallFeed; - public remoteScreensharingFeed?: CallFeed; - - public reject = jest.fn(); - public answerWithCallFeeds = jest.fn(); - public hangup = jest.fn(); - - public sendMetadataUpdate = jest.fn(); - - public on = jest.fn(); - public removeListener = jest.fn(); - - public getOpponentMember(): Partial { - return this.opponentMember; - } - - public getOpponentDeviceId(): string { - return this.opponentDeviceId; - } - - public typed(): MatrixCall { return this as unknown as MatrixCall; } -} - describe('Group Call', function() { beforeEach(function() { installWebRTCMocks(); @@ -351,7 +315,7 @@ describe('Group Call', function() { }); describe("call feeds changing", () => { - let call: MockCall; + let call: MockMatrixCall; const currentFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("current")); const newFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("new")); @@ -361,13 +325,13 @@ describe('Group Call', function() { jest.spyOn(groupCall, "emit"); - call = new MockCall(room.roomId, groupCall.groupCallId); + call = new MockMatrixCall(room.roomId, groupCall.groupCallId); await groupCall.create(); }); it("ignores changes, if we can't get user id of opponent", async () => { - const call = new MockCall(room.roomId, groupCall.groupCallId); + const call = new MockMatrixCall(room.roomId, groupCall.groupCallId); jest.spyOn(call, "getOpponentMember").mockReturnValue({ userId: undefined }); // @ts-ignore Mock @@ -514,10 +478,11 @@ describe('Group Call', function() { }); it("sends metadata updates before unmuting in PTT mode", async () => { - const mockCall = new MockCall(FAKE_ROOM_ID, groupCall.groupCallId); + const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId); + // @ts-ignore groupCall.calls.set( mockCall.getOpponentMember() as RoomMember, - new Map([[mockCall.getOpponentDeviceId(), mockCall.typed()]]), + new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]), ); let metadataUpdateResolve: () => void; @@ -539,10 +504,11 @@ describe('Group Call', function() { }); it("sends metadata updates after muting in PTT mode", async () => { - const mockCall = new MockCall(FAKE_ROOM_ID, groupCall.groupCallId); + const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId); + // @ts-ignore groupCall.calls.set( mockCall.getOpponentMember() as RoomMember, - new Map([[mockCall.getOpponentDeviceId(), mockCall.typed()]]), + new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]), ); // the call starts muted, so unmute to get in the right state to test @@ -698,6 +664,7 @@ describe('Group Call', function() { expect(client1.sendToDevice).toHaveBeenCalled(); + // @ts-ignore const oldCall = groupCall1.calls.get( groupCall1.room.getMember(client2.userId)!, )!.get(client2.deviceId)!; @@ -719,6 +686,7 @@ describe('Group Call', function() { // to even be created... let newCall: MatrixCall | undefined; while ( + // @ts-ignore (newCall = groupCall1.calls.get( groupCall1.room.getMember(client2.userId)!, )?.get(client2.deviceId)) === undefined @@ -763,6 +731,7 @@ describe('Group Call', function() { groupCall1.setMicrophoneMuted(false); groupCall1.setLocalVideoMuted(false); + // @ts-ignore const call = groupCall1.calls.get( groupCall1.room.getMember(client2.userId)!, )!.get(client2.deviceId)!; @@ -874,7 +843,10 @@ describe('Group Call', function() { // It takes a bit of time for the calls to get created await sleep(10); - const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!; + // @ts-ignore + const call = groupCall.calls + .get(groupCall.room.getMember(FAKE_USER_ID_2)!)! + .get(FAKE_DEVICE_ID_2)!; call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember; // @ts-ignore Mock call.pushRemoteFeed(new MockMediaStream("stream", [ @@ -897,7 +869,10 @@ describe('Group Call', function() { // It takes a bit of time for the calls to get created await sleep(10); - const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!; + // @ts-ignore + const call = groupCall.calls + .get(groupCall.room.getMember(FAKE_USER_ID_2)!)! + .get(FAKE_DEVICE_ID_2)!; call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember; // @ts-ignore Mock call.pushRemoteFeed(new MockMediaStream("stream", [ @@ -939,7 +914,7 @@ describe('Group Call', function() { }); it("ignores incoming calls for other rooms", async () => { - const mockCall = new MockCall("!someotherroom.fake.dummy", groupCall.groupCallId); + const mockCall = new MockMatrixCall("!someotherroom.fake.dummy", groupCall.groupCallId); mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall); @@ -948,7 +923,7 @@ describe('Group Call', function() { }); it("rejects incoming calls for the wrong group call", async () => { - const mockCall = new MockCall(room.roomId, "not " + groupCall.groupCallId); + const mockCall = new MockMatrixCall(room.roomId, "not " + groupCall.groupCallId); mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall); @@ -956,7 +931,7 @@ describe('Group Call', function() { }); it("ignores incoming calls not in the ringing state", async () => { - const mockCall = new MockCall(room.roomId, groupCall.groupCallId); + const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId); mockCall.state = CallState.Connected; mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall); @@ -966,12 +941,13 @@ describe('Group Call', function() { }); it("answers calls for the right room & group call ID", async () => { - const mockCall = new MockCall(room.roomId, groupCall.groupCallId); + const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId); mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall); expect(mockCall.reject).not.toHaveBeenCalled(); expect(mockCall.answerWithCallFeeds).toHaveBeenCalled(); + // @ts-ignore expect(groupCall.calls).toEqual(new Map([[ groupCall.room.getMember(FAKE_USER_ID_1)!, new Map([[FAKE_DEVICE_ID_1, mockCall]]), @@ -979,8 +955,8 @@ describe('Group Call', function() { }); it("replaces calls if it already has one with the same user", async () => { - const oldMockCall = new MockCall(room.roomId, groupCall.groupCallId); - const newMockCall = new MockCall(room.roomId, groupCall.groupCallId); + const oldMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId); + const newMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId); newMockCall.opponentMember = oldMockCall.opponentMember; // Ensure referential equality newMockCall.callId = "not " + oldMockCall.callId; @@ -989,6 +965,7 @@ describe('Group Call', function() { expect(oldMockCall.hangup).toHaveBeenCalled(); expect(newMockCall.answerWithCallFeeds).toHaveBeenCalled(); + // @ts-ignore expect(groupCall.calls).toEqual(new Map([[ groupCall.room.getMember(FAKE_USER_ID_1)!, new Map([[FAKE_DEVICE_ID_1, newMockCall]]), @@ -999,7 +976,7 @@ describe('Group Call', function() { // First we leave the call since we have already entered groupCall.leave(); - const call = new MockCall(room.roomId, groupCall.groupCallId); + const call = new MockMatrixCall(room.roomId, groupCall.groupCallId); mockClient.callEventHandler!.calls = new Map([ [call.callId, call.typed()], ]); @@ -1072,7 +1049,10 @@ describe('Group Call', function() { // It takes a bit of time for the calls to get created await sleep(10); - const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!; + // @ts-ignore + const call = groupCall.calls + .get(groupCall.room.getMember(FAKE_USER_ID_2)!)! + .get(FAKE_DEVICE_ID_2)!; call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember; call.onNegotiateReceived({ getContent: () => ({ diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 8b4882c960a..ad5e6668bcd 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -334,7 +334,6 @@ export class MatrixCall extends TypedEventEmitter; @@ -482,6 +482,16 @@ export class MatrixCall extends TypedEventEmitter { @@ -1762,7 +1774,7 @@ export class MatrixCall extends TypedEventEmitter { this.inviteTimeout = undefined; if (this.state === CallState.InviteSent) { @@ -2088,7 +2100,7 @@ export class MatrixCall extends TypedEventEmitter { @@ -2112,7 +2124,7 @@ export class MatrixCall extends TypedEventEmitter void; [CallFeedEvent.LocalVolumeChanged]: (localVolume: number) => void; [CallFeedEvent.VolumeChanged]: (volume: number) => void; + [CallFeedEvent.ConnectedChanged]: (connected: boolean) => void; [CallFeedEvent.Speaking]: (speaking: boolean) => void; [CallFeedEvent.Disposed]: () => void; }; @@ -69,6 +76,7 @@ export class CallFeed extends TypedEventEmitter public speakingVolumeSamples: number[]; private client: MatrixClient; + private call?: MatrixCall; private roomId?: string; private audioMuted: boolean; private videoMuted: boolean; @@ -81,11 +89,13 @@ export class CallFeed extends TypedEventEmitter private speaking = false; private volumeLooperTimeout?: ReturnType; private _disposed = false; + private _connected = false; public constructor(opts: ICallFeedOpts) { super(); this.client = opts.client; + this.call = opts.call; this.roomId = opts.roomId; this.userId = opts.userId; this.deviceId = opts.deviceId; @@ -101,6 +111,21 @@ export class CallFeed extends TypedEventEmitter if (this.hasAudioTrack) { this.initVolumeMeasuring(); } + + if (opts.call) { + opts.call.addListener(CallEvent.State, this.onCallState); + this.onCallState(opts.call.state); + } + } + + public get connected(): boolean { + // Local feeds are always considered connected + return this.isLocal() || this._connected; + } + + private set connected(connected: boolean) { + this._connected = connected; + this.emit(CallFeedEvent.ConnectedChanged, this.connected); } private get hasAudioTrack(): boolean { @@ -145,6 +170,14 @@ export class CallFeed extends TypedEventEmitter this.emit(CallFeedEvent.NewStream, this.stream); }; + private onCallState = (state: CallState): void => { + if (state === CallState.Connected) { + this.connected = true; + } else if (state === CallState.Connecting) { + this.connected = false; + } + }; + /** * Returns callRoom member * @returns member of the callRoom @@ -297,6 +330,7 @@ export class CallFeed extends TypedEventEmitter public dispose(): void { clearTimeout(this.volumeLooperTimeout); this.stream?.removeEventListener("addtrack", this.onAddTrack); + this.call?.removeListener(CallEvent.State, this.onCallState); if (this.audioContext) { this.audioContext = undefined; this.analyser = undefined; diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index fde46182bc1..5941b6a3728 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -168,11 +168,11 @@ export class GroupCall extends TypedEventEmitter< public localCallFeed?: CallFeed; public localScreenshareFeed?: CallFeed; public localDesktopCapturerSourceId?: string; - public readonly calls = new Map>(); public readonly userMediaFeeds: CallFeed[] = []; public readonly screenshareFeeds: CallFeed[] = []; public groupCallId: string; + private readonly calls = new Map>(); // RoomMember -> device ID -> MatrixCall private callHandlers = new Map>(); // User ID -> device ID -> handlers private activeSpeakerLoopInterval?: ReturnType; private retryCallLoopInterval?: ReturnType; From 683e7fba4ab3861bf9c822308fd69f17016618f1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 6 Dec 2022 10:31:48 +0000 Subject: [PATCH 23/60] Add a message ID on each to-device message (#2938) To make it easier to track down where to-device messages are getting lost, add a custom property to each one, and log its value. Synapse will also log this property. --- package.json | 4 ++- spec/unit/crypto.spec.ts | 37 +++++++++++++-------- spec/unit/crypto/algorithms/megolm.spec.ts | 3 ++ src/@types/event.ts | 2 ++ src/ToDeviceMessageQueue.ts | 10 ++++-- src/crypto/OutgoingRoomKeyRequestManager.ts | 9 +++-- src/crypto/SecretStorage.ts | 5 +++ src/crypto/algorithms/megolm.ts | 18 +++++++--- src/crypto/index.ts | 10 ++++-- src/webrtc/call.ts | 4 ++- yarn.lock | 10 ++++++ 11 files changed, 85 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 5cfc418633d..02a849ac859 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,8 @@ "p-retry": "4", "qs": "^6.9.6", "sdp-transform": "^2.14.1", - "unhomoglyph": "^1.0.6" + "unhomoglyph": "^1.0.6", + "uuid": "7" }, "devDependencies": { "@babel/cli": "^7.12.10", @@ -86,6 +87,7 @@ "@types/jest": "^29.0.0", "@types/node": "18", "@types/sdp-transform": "^2.4.5", + "@types/uuid": "7", "@typescript-eslint/eslint-plugin": "^5.6.0", "@typescript-eslint/parser": "^5.6.0", "allchange": "^1.0.6", diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index 68c8264b6f8..b319a5f26ad 100644 --- a/spec/unit/crypto.spec.ts +++ b/spec/unit/crypto.spec.ts @@ -1010,18 +1010,24 @@ describe("Crypto", function() { it("encrypts and sends to devices", async () => { client.httpBackend - .when("PUT", "/sendToDevice/m.room.encrypted", { - messages: { - "@bob:example.org": { - bobweb: encryptedPayload, - bobmobile: encryptedPayload, - }, - "@carol:example.org": { - caroldesktop: encryptedPayload, + .when("PUT", "/sendToDevice/m.room.encrypted") + .check((request) => { + const data = request.data; + delete data.messages["@bob:example.org"]["bobweb"]["org.matrix.msgid"]; + delete data.messages["@bob:example.org"]["bobmobile"]["org.matrix.msgid"]; + delete data.messages["@carol:example.org"]["caroldesktop"]["org.matrix.msgid"]; + expect(data).toStrictEqual({ + messages: { + "@bob:example.org": { + bobweb: encryptedPayload, + bobmobile: encryptedPayload, + }, + "@carol:example.org": { + caroldesktop: encryptedPayload, + }, }, - }, - }) - .respond(200, {}); + }); + }).respond(200, {}); await Promise.all([ client.client.encryptAndSendToDevices( @@ -1044,9 +1050,14 @@ describe("Crypto", function() { }); client.httpBackend - .when("PUT", "/sendToDevice/m.room.encrypted", { + .when("PUT", "/sendToDevice/m.room.encrypted") + .check((req) => { + const data = req.data; + delete data.messages["@bob:example.org"]["bobweb"]["org.matrix.msgid"]; // Carol is nowhere to be seen - messages: { "@bob:example.org": { bobweb: encryptedPayload } }, + expect(data).toStrictEqual({ + messages: { "@bob:example.org": { bobweb: encryptedPayload } }, + }); }) .respond(200, {}); diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index a1519d4ab61..b631201996a 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -558,7 +558,9 @@ describe("MegolmDecryption", function() { const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0]; expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/); delete contentMap["@bob:example.com"].bobdevice1.session_id; + delete contentMap["@bob:example.com"].bobdevice1["org.matrix.msgid"]; delete contentMap["@bob:example.com"].bobdevice2.session_id; + delete contentMap["@bob:example.com"].bobdevice2["org.matrix.msgid"]; expect(contentMap).toStrictEqual({ '@bob:example.com': { bobdevice1: { @@ -755,6 +757,7 @@ describe("MegolmDecryption", function() { expect(aliceClient.sendToDevice).toHaveBeenCalled(); const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0]; expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/); + delete contentMap["@bob:example.com"]["bobdevice"]["org.matrix.msgid"]; expect(contentMap).toStrictEqual({ '@bob:example.com': { bobdevice: { diff --git a/src/@types/event.ts b/src/@types/event.ts index 168097925b2..ab07c01d776 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -120,6 +120,8 @@ export enum RoomType { ElementVideo = "io.element.video", } +export const ToDeviceMessageId = "org.matrix.msgid"; + /** * Identifier for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088) * room purpose. Note that this reference is UNSTABLE and subject to breaking changes, diff --git a/src/ToDeviceMessageQueue.ts b/src/ToDeviceMessageQueue.ts index 0b5b1786a63..bf881395b2e 100644 --- a/src/ToDeviceMessageQueue.ts +++ b/src/ToDeviceMessageQueue.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { ToDeviceMessageId } from './@types/event'; import { logger } from "./logger"; import { MatrixError, MatrixClient } from "./matrix"; import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage"; @@ -54,12 +55,15 @@ export class ToDeviceMessageQueue { txnId: this.client.makeTxnId(), }; batches.push(batchWithTxnId); - const recips = batchWithTxnId.batch.map((msg) => `${msg.userId}:${msg.deviceId}`); - logger.info(`Created batch of to-device messages with txn id ${batchWithTxnId.txnId} for ${recips}`); + const msgmap = batchWithTxnId.batch.map( + (msg) => `${msg.userId}/${msg.deviceId} (msgid ${msg.payload[ToDeviceMessageId]})`, + ); + logger.info( + `Enqueuing batch of to-device messages. type=${batch.eventType} txnid=${batchWithTxnId.txnId}`, msgmap, + ); } await this.client.store.saveToDeviceBatches(batches); - logger.info(`Enqueued to-device messages with txn ids ${batches.map((batch) => batch.txnId)}`); this.sendQueue(); } diff --git a/src/crypto/OutgoingRoomKeyRequestManager.ts b/src/crypto/OutgoingRoomKeyRequestManager.ts index 42723434778..cb93851b9d1 100644 --- a/src/crypto/OutgoingRoomKeyRequestManager.ts +++ b/src/crypto/OutgoingRoomKeyRequestManager.ts @@ -14,11 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { v4 as uuidv4 } from "uuid"; + import { logger } from '../logger'; import { MatrixClient } from "../client"; import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index"; import { CryptoStore, OutgoingRoomKeyRequest } from './store/base'; -import { EventType } from "../@types/event"; +import { EventType, ToDeviceMessageId } from "../@types/event"; /** * Internal module. Management of outgoing room key requests. @@ -483,7 +485,10 @@ export class OutgoingRoomKeyRequestManager { if (!contentMap[recip.userId]) { contentMap[recip.userId] = {}; } - contentMap[recip.userId][recip.deviceId] = message; + contentMap[recip.userId][recip.deviceId] = { + ...message, + [ToDeviceMessageId]: uuidv4(), + }; } return this.baseApis.sendToDevice(EventType.RoomKeyRequest, contentMap, txnId); diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts index 5c13ba4b0bb..a7e1e7d738f 100644 --- a/src/crypto/SecretStorage.ts +++ b/src/crypto/SecretStorage.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { v4 as uuidv4 } from 'uuid'; + import { logger } from '../logger'; import * as olmlib from './olmlib'; import { encodeBase64 } from './olmlib'; @@ -25,6 +27,7 @@ import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client"; import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api'; import { TypedEventEmitter } from '../models/typed-event-emitter'; import { defer, IDeferred } from "../utils"; +import { ToDeviceMessageId } from "../@types/event"; export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; @@ -407,6 +410,7 @@ export class SecretStorage { action: "request", requesting_device_id: this.baseApis.deviceId, request_id: requestId, + [ToDeviceMessageId]: uuidv4(), }; const toDevice = {}; for (const device of devices) { @@ -490,6 +494,7 @@ export class SecretStorage { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key, ciphertext: {}, + [ToDeviceMessageId]: uuidv4(), }; await olmlib.ensureOlmSessionsForDevices( this.baseApis.crypto!.olmDevice, diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index cbad327a608..1d164cc2334 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -20,6 +20,8 @@ limitations under the License. * @module crypto/algorithms/megolm */ +import { v4 as uuidv4 } from "uuid"; + import { logger } from '../../logger'; import * as olmlib from "../olmlib"; import { @@ -37,7 +39,7 @@ import { DeviceInfo } from "../deviceinfo"; import { IOlmSessionResult } from "../olmlib"; import { DeviceInfoMap } from "../DeviceList"; import { MatrixEvent } from "../../models/event"; -import { EventType, MsgType } from '../../@types/event'; +import { EventType, MsgType, ToDeviceMessageId } from '../../@types/event'; import { IEncryptedContent, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index"; import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager'; import { OlmGroupSessionExtraData } from "../../@types/crypto"; @@ -655,9 +657,13 @@ class MegolmEncryption extends EncryptionAlgorithm { const deviceInfo = blockedInfo.deviceInfo; const deviceId = deviceInfo.deviceId; - const message = Object.assign({}, payload); - message.code = blockedInfo.code; - message.reason = blockedInfo.reason; + const message = { + ...payload, + code: blockedInfo.code, + reason: blockedInfo.reason, + [ToDeviceMessageId]: uuidv4(), + }; + if (message.code === "m.no_olm") { delete message.room_id; delete message.session_id; @@ -759,6 +765,7 @@ class MegolmEncryption extends EncryptionAlgorithm { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key, ciphertext: {}, + [ToDeviceMessageId]: uuidv4(), }; await olmlib.encryptMessageForDevice( encryptedContent.ciphertext, @@ -1667,6 +1674,7 @@ class MegolmDecryption extends DecryptionAlgorithm { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key, ciphertext: {}, + [ToDeviceMessageId]: uuidv4(), }; await olmlib.encryptMessageForDevice( encryptedContent.ciphertext, @@ -1748,6 +1756,7 @@ class MegolmDecryption extends DecryptionAlgorithm { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key, ciphertext: {}, + [ToDeviceMessageId]: uuidv4, }; return this.olmlib.encryptMessageForDevice( @@ -1923,6 +1932,7 @@ class MegolmDecryption extends DecryptionAlgorithm { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: {}, + [ToDeviceMessageId]: uuidv4(), }; contentMap[userId][deviceInfo.deviceId] = encryptedContent; promises.push( diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 2d6995a58bb..d71fadf9073 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -22,9 +22,10 @@ limitations under the License. */ import anotherjson from "another-json"; +import { v4 as uuidv4 } from "uuid"; import type { PkDecryption, PkSigning } from "@matrix-org/olm"; -import { EventType } from "../@types/event"; +import { EventType, ToDeviceMessageId } from "../@types/event"; import { TypedReEmitter } from '../ReEmitter'; import { logger } from '../logger'; import { IExportedDevice, OlmDevice } from "./OlmDevice"; @@ -234,6 +235,7 @@ export interface IEncryptedContent { algorithm: string; sender_key: string; ciphertext: Record; + [ToDeviceMessageId]: string; } /* eslint-enable camelcase */ @@ -3173,6 +3175,7 @@ export class Crypto extends TypedEventEmitter { try { - logger.log(`received to_device ${event.getType()} from: ` + - `${event.getSender()} id: ${event.getId()}`); + logger.log(`received to-device ${event.getType()} from: ` + + `${event.getSender()} id: ${event.getContent()[ToDeviceMessageId]}`); if (event.getType() == "m.room_key" || event.getType() == "m.forwarded_room_key") { @@ -3516,6 +3519,7 @@ export class Crypto extends TypedEventEmitter Date: Tue, 6 Dec 2022 12:34:38 +0000 Subject: [PATCH 24/60] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 98dfac3d3bf..9ad98499a73 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "keywords": [ "matrix-org" ], - "main": "./lib/index.js", + "main": "./src/index.ts", "browser": "./lib/browser-index.js", "matrix_src_main": "./src/index.ts", "matrix_src_browser": "./src/browser-index.js", @@ -138,6 +138,5 @@ "outputDirectory": "coverage", "outputName": "jest-sonar-report.xml", "relativePaths": true - }, - "typings": "./lib/index.d.ts" + } } From 6f81371e61ff5ebe9966a4aca92a0c73513d4fa5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 6 Dec 2022 16:45:21 +0000 Subject: [PATCH 25/60] Fix the message ID on key-share messages (#2945) https://github.com/matrix-org/matrix-js-sdk/pull/2938 introduced message IDs on outgoing to-device messages, but a typo meant that the IDs on key-share messages were excessive. --- src/crypto/algorithms/megolm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index 1d164cc2334..9ba33e32a78 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -1756,7 +1756,7 @@ class MegolmDecryption extends DecryptionAlgorithm { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key, ciphertext: {}, - [ToDeviceMessageId]: uuidv4, + [ToDeviceMessageId]: uuidv4(), }; return this.olmlib.encryptMessageForDevice( From 8d018f9c2d3376c24a601722c53302fe3adbe48c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Dec 2022 18:21:44 +0000 Subject: [PATCH 26/60] Enable noImplicitAny (#2895) * Stash noImplicitAny work * Enable noImplicitAny * Update olm * Fun * Fix msgid stuff * Fix tests * Attempt to fix Browserify --- .github/workflows/static_analysis.yml | 37 +- package.json | 8 +- spec/TestClient.ts | 2 +- spec/browserify/setupTests.ts | 16 +- spec/browserify/sync-browserify.spec.ts | 21 +- spec/integ/devicelist-integ.spec.ts | 91 +-- spec/integ/matrix-client-crypto.spec.ts | 140 ++-- .../matrix-client-event-timeline.spec.ts | 62 +- spec/integ/matrix-client-methods.spec.ts | 6 +- spec/integ/matrix-client-opts.spec.ts | 8 +- .../integ/matrix-client-room-timeline.spec.ts | 29 +- spec/integ/matrix-client-syncing.spec.ts | 597 +++++++++--------- spec/integ/megolm-backup.spec.ts | 13 +- spec/integ/sliding-sync-sdk.spec.ts | 16 +- spec/integ/sliding-sync.spec.ts | 50 +- spec/test-utils/test-utils.ts | 6 +- spec/unit/crypto.spec.ts | 46 +- spec/unit/crypto/CrossSigningInfo.spec.ts | 2 +- spec/unit/crypto/DeviceList.spec.ts | 7 +- spec/unit/crypto/algorithms/megolm.spec.ts | 29 +- spec/unit/crypto/algorithms/olm.spec.ts | 23 +- spec/unit/crypto/backup.spec.ts | 162 ++--- spec/unit/crypto/cross-signing.spec.ts | 38 +- spec/unit/crypto/crypto-utils.ts | 9 +- spec/unit/crypto/secrets.spec.ts | 62 +- spec/unit/crypto/verification/sas.spec.ts | 64 +- .../verification/secret_request.spec.ts | 4 +- spec/unit/crypto/verification/util.ts | 20 +- .../verification/verification_request.spec.ts | 12 +- spec/unit/matrix-client.spec.ts | 272 ++++---- spec/unit/models/MSC3089TreeSpace.spec.ts | 6 +- spec/unit/models/event.spec.ts | 4 +- spec/unit/notifications.spec.ts | 2 +- spec/unit/read-receipt.spec.ts | 2 +- spec/unit/realtime-callbacks.spec.ts | 2 +- spec/unit/rendezvous/rendezvous.spec.ts | 2 +- spec/unit/room-member.spec.ts | 65 +- spec/unit/room.spec.ts | 203 +++--- spec/unit/scheduler.spec.ts | 45 +- spec/unit/sync-accumulator.spec.ts | 34 +- spec/unit/utils.spec.ts | 12 +- spec/unit/webrtc/call.spec.ts | 6 +- src/@types/read_receipts.ts | 2 +- src/autodiscovery.ts | 24 +- src/{browser-index.js => browser-index.ts} | 21 +- src/client.ts | 97 +-- src/crypto/CrossSigning.ts | 1 + src/crypto/EncryptionSetup.ts | 4 +- src/crypto/OlmDevice.ts | 53 +- src/crypto/SecretStorage.ts | 36 +- src/crypto/algorithms/base.ts | 12 +- src/crypto/algorithms/megolm.ts | 38 +- src/crypto/algorithms/olm.ts | 12 +- src/crypto/backup.ts | 6 +- src/crypto/dehydration.ts | 2 +- src/crypto/deviceinfo.ts | 3 +- src/crypto/index.ts | 53 +- src/crypto/olmlib.ts | 12 +- src/crypto/store/base.ts | 2 +- src/crypto/store/localStorage-crypto-store.ts | 14 +- src/crypto/verification/QRCode.ts | 10 +- src/crypto/verification/SAS.ts | 42 +- src/filter-component.ts | 8 +- src/filter.ts | 7 +- src/models/event.ts | 27 +- src/models/invites-ignorer.ts | 10 +- src/models/room-state.ts | 38 +- src/models/room-summary.ts | 4 +- src/models/room.ts | 6 +- src/pushprocessor.ts | 4 +- src/scheduler.ts | 2 +- src/sliding-sync-sdk.ts | 110 +++- src/sliding-sync.ts | 20 +- src/store/index.ts | 2 +- src/store/indexeddb-local-backend.ts | 4 +- src/store/memory.ts | 2 +- src/store/stub.ts | 2 +- src/sync-accumulator.ts | 71 ++- src/sync.ts | 16 +- src/utils.ts | 16 +- src/webrtc/call.ts | 4 +- tsconfig.json | 1 - yarn.lock | 6 +- 83 files changed, 1613 insertions(+), 1426 deletions(-) rename src/{browser-index.js => browser-index.ts} (76%) diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 5f612727815..dfcaf62af0b 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -64,7 +64,7 @@ jobs: - name: Generate Docs run: "yarn run gendoc" - + - name: Upload Artifact uses: actions/upload-artifact@v3 with: @@ -72,38 +72,3 @@ jobs: path: _docs # We'll only use this in a workflow_run, then we're done with it retention-days: 1 - - tsc-strict: - name: Typescript Strict Error Checker - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - permissions: - pull-requests: read - checks: write - steps: - - uses: actions/checkout@v3 - - - name: Get diff lines - id: diff - uses: Equip-Collaboration/diff-line-numbers@v1.0.0 - with: - include: '["\\.tsx?$"]' - - - name: Detecting files changed - id: files - uses: futuratrepadeira/changed-files@v4.0.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - pattern: '^.*\.tsx?$' - - - uses: t3chguy/typescript-check-action@main - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - use-check: false - check-fail-mode: added - output-behaviour: annotate - ts-extra-args: '--noImplicitAny' - files-changed: ${{ steps.files.outputs.files_updated }} - files-added: ${{ steps.files.outputs.files_created }} - files-deleted: ${{ steps.files.outputs.files_deleted }} - line-numbers: ${{ steps.diff.outputs.lineNumbers }} diff --git a/package.json b/package.json index 9ad98499a73..76529468dc1 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", "build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly", "build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src", - "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", + "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", "build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js", "gendoc": "typedoc", "lint": "yarn lint:types && yarn lint:js", @@ -33,9 +33,9 @@ "matrix-org" ], "main": "./src/index.ts", - "browser": "./lib/browser-index.js", + "browser": "./lib/browser-index.ts", "matrix_src_main": "./src/index.ts", - "matrix_src_browser": "./src/browser-index.js", + "matrix_src_browser": "./src/browser-index.ts", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", "author": "matrix.org", @@ -80,7 +80,7 @@ "@babel/preset-typescript": "^7.12.7", "@babel/register": "^7.12.10", "@casualbot/jest-sonar-reporter": "^2.2.5", - "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz", "@types/bs58": "^4.0.1", "@types/content-type": "^1.1.5", "@types/domexception": "^4.0.0", diff --git a/spec/TestClient.ts b/spec/TestClient.ts index 249f5b39e16..02d26b6a76f 100644 --- a/spec/TestClient.ts +++ b/spec/TestClient.ts @@ -183,7 +183,7 @@ export class TestClient { this.httpBackend.when('POST', '/keys/query').respond( 200, (_path, content) => { Object.keys(response.device_keys).forEach((userId) => { - expect(content.device_keys![userId]).toEqual([]); + expect((content.device_keys! as Record)[userId]).toEqual([]); }); return response; }); diff --git a/spec/browserify/setupTests.ts b/spec/browserify/setupTests.ts index a92a70e23b3..789c0218db3 100644 --- a/spec/browserify/setupTests.ts +++ b/spec/browserify/setupTests.ts @@ -15,19 +15,7 @@ limitations under the License. */ import "../../dist/browser-matrix"; // uses browser-matrix instead of the src -import type { MatrixClient, ClientEvent } from "../../src"; - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace NodeJS { - interface Global { - matrixcs: { - MatrixClient: typeof MatrixClient; - ClientEvent: typeof ClientEvent; - }; - } - } -} +import type { default as BrowserMatrix } from "../../src/browser-index"; // stub for browser-matrix browserify tests // @ts-ignore @@ -43,4 +31,4 @@ afterAll(() => { global.matrixcs = { ...global.matrixcs, timeoutSignal: () => new AbortController().signal, -}; +} as typeof BrowserMatrix; diff --git a/spec/browserify/sync-browserify.spec.ts b/spec/browserify/sync-browserify.spec.ts index 172cb0c475d..6898c3e1cdc 100644 --- a/spec/browserify/sync-browserify.spec.ts +++ b/spec/browserify/sync-browserify.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import HttpBackend from "matrix-mock-request"; -import "./setupTests";// uses browser-matrix instead of the src +import "./setupTests"; // uses browser-matrix instead of the src import type { MatrixClient } from "../../src"; const USER_ID = "@user:test.server"; @@ -65,15 +65,16 @@ describe("Browserify Test", function() { const syncData = { next_batch: "batch1", rooms: { - join: {}, - }, - }; - syncData.rooms.join[ROOM_ID] = { - timeline: { - events: [ - event, - ], - limited: false, + join: { + [ROOM_ID]: { + timeline: { + events: [ + event, + ], + limited: false, + }, + }, + }, }, }; diff --git a/spec/integ/devicelist-integ.spec.ts b/spec/integ/devicelist-integ.spec.ts index acd8f9c8081..5b980149b02 100644 --- a/spec/integ/devicelist-integ.spec.ts +++ b/spec/integ/devicelist-integ.spec.ts @@ -30,7 +30,7 @@ const ROOM_ID = "!room:id"; * * @return {object} sync response */ -function getSyncResponse(roomMembers) { +function getSyncResponse(roomMembers: string[]) { const stateEvents = [ testUtils.mkEvent({ type: 'm.room.encryption', @@ -43,12 +43,10 @@ function getSyncResponse(roomMembers) { Array.prototype.push.apply( stateEvents, - roomMembers.map( - (m) => testUtils.mkMembership({ - mship: 'join', - sender: m, - }), - ), + roomMembers.map((m) => testUtils.mkMembership({ + mship: 'join', + sender: m, + })), ); const syncResponse = { @@ -73,8 +71,8 @@ describe("DeviceList management:", function() { return; } - let sessionStoreBackend; - let aliceTestClient; + let aliceTestClient: TestClient; + let sessionStoreBackend: Storage; async function createTestClient() { const testClient = new TestClient( @@ -97,7 +95,10 @@ describe("DeviceList management:", function() { }); it("Alice shouldn't do a second /query for non-e2e-capable devices", function() { - aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } }); + aliceTestClient.expectKeyQuery({ + device_keys: { '@alice:localhost': {} }, + failures: {}, + }); return aliceTestClient.start().then(function() { const syncResponse = getSyncResponse(['@bob:xyz']); aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse); @@ -138,7 +139,10 @@ describe("DeviceList management:", function() { it.skip("We should not get confused by out-of-order device query responses", () => { // https://github.com/vector-im/element-web/issues/3126 - aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } }); + aliceTestClient.expectKeyQuery({ + device_keys: { '@alice:localhost': {} }, + failures: {}, + }); return aliceTestClient.start().then(() => { aliceTestClient.httpBackend.when('GET', '/sync').respond( 200, getSyncResponse(['@bob:xyz', '@chris:abc'])); @@ -164,11 +168,12 @@ describe("DeviceList management:", function() { aliceTestClient.httpBackend.flush('/keys/query', 1).then( () => aliceTestClient.httpBackend.flush('/send/', 1), ), - aliceTestClient.client.crypto.deviceList.saveIfDirty(), + aliceTestClient.client.crypto!.deviceList.saveIfDirty(), ]); }).then(() => { - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - expect(data.syncToken).toEqual(1); + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + expect(data!.syncToken).toEqual(1); }); // invalidate bob's and chris's device lists in separate syncs @@ -201,15 +206,16 @@ describe("DeviceList management:", function() { return aliceTestClient.httpBackend.flush('/keys/query', 1); }).then((flushed) => { expect(flushed).toEqual(0); - return aliceTestClient.client.crypto.deviceList.saveIfDirty(); + return aliceTestClient.client.crypto!.deviceList.saveIfDirty(); }).then(() => { - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + const bobStat = data!.trackingStatus['@bob:xyz']; if (bobStat != 1 && bobStat != 2) { throw new Error('Unexpected status for bob: wanted 1 or 2, got ' + bobStat); } - const chrisStat = data.trackingStatus['@chris:abc']; + const chrisStat = data!.trackingStatus['@chris:abc']; if (chrisStat != 1 && chrisStat != 2) { throw new Error( 'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat, @@ -234,12 +240,13 @@ describe("DeviceList management:", function() { // wait for the client to stop processing the response return aliceTestClient.client.downloadKeys(['@bob:xyz']); }).then(() => { - return aliceTestClient.client.crypto.deviceList.saveIfDirty(); + return aliceTestClient.client.crypto!.deviceList.saveIfDirty(); }).then(() => { - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + const bobStat = data!.trackingStatus['@bob:xyz']; expect(bobStat).toEqual(3); - const chrisStat = data.trackingStatus['@chris:abc']; + const chrisStat = data!.trackingStatus['@chris:abc']; if (chrisStat != 1 && chrisStat != 2) { throw new Error( 'Unexpected status for chris: wanted 1 or 2, got ' + bobStat, @@ -255,15 +262,16 @@ describe("DeviceList management:", function() { // wait for the client to stop processing the response return aliceTestClient.client.downloadKeys(['@chris:abc']); }).then(() => { - return aliceTestClient.client.crypto.deviceList.saveIfDirty(); + return aliceTestClient.client.crypto!.deviceList.saveIfDirty(); }).then(() => { - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; - const chrisStat = data.trackingStatus['@bob:xyz']; + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + const bobStat = data!.trackingStatus['@bob:xyz']; + const chrisStat = data!.trackingStatus['@bob:xyz']; expect(bobStat).toEqual(3); expect(chrisStat).toEqual(3); - expect(data.syncToken).toEqual(3); + expect(data!.syncToken).toEqual(3); }); }); }); @@ -285,10 +293,11 @@ describe("DeviceList management:", function() { }, ); await aliceTestClient.httpBackend.flush('/keys/query', 1); - await aliceTestClient.client.crypto.deviceList.saveIfDirty(); + await aliceTestClient.client.crypto!.deviceList.saveIfDirty(); - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + const bobStat = data!.trackingStatus['@bob:xyz']; // Alice should be tracking bob's device list expect(bobStat).toBeGreaterThan( @@ -322,10 +331,11 @@ describe("DeviceList management:", function() { ); await aliceTestClient.flushSync(); - await aliceTestClient.client.crypto.deviceList.saveIfDirty(); + await aliceTestClient.client.crypto!.deviceList.saveIfDirty(); - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + const bobStat = data!.trackingStatus['@bob:xyz']; // Alice should have marked bob's device list as untracked expect(bobStat).toEqual( @@ -359,15 +369,14 @@ describe("DeviceList management:", function() { ); await aliceTestClient.flushSync(); - await aliceTestClient.client.crypto.deviceList.saveIfDirty(); + await aliceTestClient.client.crypto!.deviceList.saveIfDirty(); - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + const bobStat = data!.trackingStatus['@bob:xyz']; // Alice should have marked bob's device list as untracked - expect(bobStat).toEqual( - 0, - ); + expect(bobStat).toEqual(0); }); }); @@ -388,9 +397,7 @@ describe("DeviceList management:", function() { const bobStat = data!.trackingStatus['@bob:xyz']; // Alice should have marked bob's device list as untracked - expect(bobStat).toEqual( - 0, - ); + expect(bobStat).toEqual(0); }); } finally { anotherTestClient.stop(); diff --git a/spec/integ/matrix-client-crypto.spec.ts b/spec/integ/matrix-client-crypto.spec.ts index 847bb8528ae..adb7dd25b9a 100644 --- a/spec/integ/matrix-client-crypto.spec.ts +++ b/spec/integ/matrix-client-crypto.spec.ts @@ -28,12 +28,14 @@ limitations under the License. // load olm before the sdk if possible import '../olm-loader'; +import type { Session } from "@matrix-org/olm"; import { logger } from '../../src/logger'; import * as testUtils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; -import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client"; +import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client"; import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix"; import { DeviceInfo } from '../../src/crypto/deviceinfo'; +import { IDeviceKeys, IOneTimeKey } from "../../src/crypto/dehydration"; let aliTestClient: TestClient; const roomId = "!room:localhost"; @@ -47,11 +49,7 @@ const bobAccessToken = "fewgfkuesa"; let aliMessages: IContent[]; let bobMessages: IContent[]; -// IMessage isn't exported by src/crypto/algorithms/olm.ts -interface OlmPayload { - type: number; - body: string; -} +type OlmPayload = ReturnType; async function bobUploadsDeviceKeys(): Promise { bobTestClient.expectDeviceKeyUpload(); @@ -71,12 +69,12 @@ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise = {}; + uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!; querier.httpBackend.when("POST", "/keys/query") - .respond(200, function(_path, content: IUploadKeysRequest) { + .respond(200, function(_path, content: IQueryKeysRequest) { expect(content.device_keys![uploader.userId!]).toEqual([]); - const result = {}; + const result: Record> = {}; result[uploader.userId!] = uploaderKeys; return { device_keys: result }; }); @@ -94,7 +92,7 @@ async function expectAliClaimKeys(): Promise { const keys = await bobTestClient.awaitOneTimeKeyUpload(); aliTestClient.httpBackend.when( "POST", "/keys/claim", - ).respond(200, function(_path, content: IUploadKeysRequest) { + ).respond(200, function(_path, content: IClaimKeysRequest) { const claimType = content.one_time_keys![bobUserId][bobDeviceId]; expect(claimType).toEqual("signed_curve25519"); let keyId = ''; @@ -105,7 +103,7 @@ async function expectAliClaimKeys(): Promise { } } } - const result = {}; + const result: Record>> = {}; result[bobUserId] = {}; result[bobUserId][bobDeviceId] = {}; result[bobUserId][bobDeviceId][keyId] = keys[keyId]; @@ -276,22 +274,21 @@ async function recvMessage( next_batch: "x", rooms: { join: { - + [roomId]: { + timeline: { + events: [ + testUtils.mkEvent({ + type: "m.room.encrypted", + room: roomId, + content: message, + sender: sender, + }), + ], + }, + }, }, }, }; - syncData.rooms.join[roomId] = { - timeline: { - events: [ - testUtils.mkEvent({ - type: "m.room.encrypted", - room: roomId, - content: message, - sender: sender, - }), - ], - }, - }; httpBackend.when("GET", "/sync").respond(200, syncData); const eventPromise = new Promise((resolve) => { @@ -335,24 +332,25 @@ function firstSync(testClient: TestClient): Promise { const syncData = { next_batch: "x", rooms: { - join: { }, - }, - }; - syncData.rooms.join[roomId] = { - state: { - events: [ - testUtils.mkMembership({ - mship: "join", - user: aliUserId, - }), - testUtils.mkMembership({ - mship: "join", - user: bobUserId, - }), - ], - }, - timeline: { - events: [], + join: { + [roomId]: { + state: { + events: [ + testUtils.mkMembership({ + mship: "join", + user: aliUserId, + }), + testUtils.mkMembership({ + mship: "join", + user: bobUserId, + }), + ], + }, + timeline: { + events: [], + }, + }, + }, }, }; @@ -424,7 +422,7 @@ describe("MatrixClient crypto", () => { }, }; - const bobKeys = {}; + const bobKeys: Record = {}; bobKeys[bobDeviceId] = bobDeviceKeys; aliTestClient.httpBackend.when( "POST", "/keys/query", @@ -460,7 +458,7 @@ describe("MatrixClient crypto", () => { }, }; - const bobKeys = {}; + const bobKeys: Record = {}; bobKeys[bobDeviceId] = bobDeviceKeys; aliTestClient.httpBackend.when( "POST", "/keys/query", @@ -515,22 +513,21 @@ describe("MatrixClient crypto", () => { next_batch: "x", rooms: { join: { - + [roomId]: { + timeline: { + events: [ + testUtils.mkEvent({ + type: "m.room.encrypted", + room: roomId, + content: message, + sender: "@bogus:sender", + }), + ], + }, + }, }, }, }; - syncData.rooms.join[roomId] = { - timeline: { - events: [ - testUtils.mkEvent({ - type: "m.room.encrypted", - room: roomId, - content: message, - sender: "@bogus:sender", - }), - ], - }, - }; bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData); const eventPromise = new Promise((resolve) => { @@ -607,20 +604,21 @@ describe("MatrixClient crypto", () => { const syncData = { next_batch: '2', rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomId] = { - state: { - events: [ - testUtils.mkEvent({ - type: 'm.room.encryption', - skey: '', - content: { - algorithm: 'm.olm.v1.curve25519-aes-sha2', + join: { + [roomId]: { + state: { + events: [ + testUtils.mkEvent({ + type: 'm.room.encryption', + skey: '', + content: { + algorithm: 'm.olm.v1.curve25519-aes-sha2', + }, + }), + ], }, - }), - ], + }, + }, }, }; diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 663af8fbfcd..5bbb8275972 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -536,20 +536,20 @@ describe("MatrixClient event timelines", function() { }; }); - let tl0; - let tl3; + let tl0: EventTimeline; + let tl3: EventTimeline; return Promise.all([ client.getEventTimeline(timelineSet, EVENTS[0].event_id!, ).then(function(tl) { expect(tl!.getEvents().length).toEqual(1); - tl0 = tl; + tl0 = tl!; return client.getEventTimeline(timelineSet, EVENTS[2].event_id!); }).then(function(tl) { expect(tl!.getEvents().length).toEqual(1); return client.getEventTimeline(timelineSet, EVENTS[3].event_id!); }).then(function(tl) { expect(tl!.getEvents().length).toEqual(1); - tl3 = tl; + tl3 = tl!; return client.getEventTimeline(timelineSet, EVENTS[1].event_id!); }).then(function(tl) { // we expect it to get merged in with event 2 @@ -953,11 +953,11 @@ describe("MatrixClient event timelines", function() { }; }); - let tl; + let tl: EventTimeline; return Promise.all([ client.getEventTimeline(timelineSet, EVENTS[0].event_id!, ).then(function(tl0) { - tl = tl0; + tl = tl0!; return client.paginateEventTimeline(tl, { backwards: true }); }).then(function(success) { expect(success).toBeTruthy(); @@ -1043,11 +1043,11 @@ describe("MatrixClient event timelines", function() { }; }); - let tl; + let tl: EventTimeline; return Promise.all([ client.getEventTimeline(timelineSet, EVENTS[0].event_id!, ).then(function(tl0) { - tl = tl0; + tl = tl0!; return client.paginateEventTimeline( tl, { backwards: false, limit: 20 }); }).then(function(success) { @@ -1569,16 +1569,17 @@ describe("MatrixClient event timelines", function() { const syncData = { next_batch: "batch1", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomId] = { - timeline: { - events: [ - event, - redaction, - ], - limited: false, + join: { + [roomId]: { + timeline: { + events: [ + event, + redaction, + ], + limited: false, + }, + }, + }, }, }; httpBackend.when("GET", "/sync").respond(200, syncData); @@ -1595,18 +1596,19 @@ describe("MatrixClient event timelines", function() { const sync2 = { next_batch: "batch2", rooms: { - join: {}, - }, - }; - sync2.rooms.join[roomId] = { - timeline: { - events: [ - utils.mkMessage({ - user: otherUserId, msg: "world", - }), - ], - limited: true, - prev_batch: "newerTok", + join: { + [roomId]: { + timeline: { + events: [ + utils.mkMessage({ + user: otherUserId, msg: "world", + }), + ], + limited: true, + prev_batch: "newerTok", + }, + }, + }, }, }; httpBackend.when("GET", "/sync").respond(200, sync2); diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index f2ff9e6df77..93c3d353727 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -618,13 +618,13 @@ describe("MatrixClient", function() { }); describe("partitionThreadedEvents", function() { - let room; + let room: Room; beforeEach(() => { room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client!, userId); }); it("returns empty arrays when given an empty arrays", function() { - const events = []; + const events: MatrixEvent[] = []; const [timeline, threaded] = room.partitionThreadedEvents(events); expect(timeline).toEqual([]); expect(threaded).toEqual([]); @@ -1645,7 +1645,7 @@ const buildEventCreate = () => new MatrixEvent({ "user_id": "@andybalaam-test1:matrix.org", }); -function assertObjectContains(obj: object, expected: any): void { +function assertObjectContains(obj: Record, expected: any): void { for (const k in expected) { if (expected.hasOwnProperty(k)) { expect(obj[k]).toEqual(expected[k]); diff --git a/spec/integ/matrix-client-opts.spec.ts b/spec/integ/matrix-client-opts.spec.ts index 5ea4fba7718..41532ea526b 100644 --- a/spec/integ/matrix-client-opts.spec.ts +++ b/spec/integ/matrix-client-opts.spec.ts @@ -1,7 +1,7 @@ import HttpBackend from "matrix-mock-request"; import * as utils from "../test-utils/test-utils"; -import { MatrixClient } from "../../src/matrix"; +import { ClientEvent, MatrixClient } from "../../src/matrix"; import { MatrixScheduler } from "../../src/scheduler"; import { MemoryStore } from "../../src/store/memory"; import { MatrixError } from "../../src/http-api"; @@ -65,7 +65,7 @@ describe("MatrixClient opts", function() { }); describe("without opts.store", function() { - let client; + let client: MatrixClient; beforeEach(function() { client = new MatrixClient({ fetchFn: httpBackend.fetchFn as typeof global.fetch, @@ -98,7 +98,7 @@ describe("MatrixClient opts", function() { "m.room.message", "m.room.name", "m.room.member", "m.room.member", "m.room.create", ]; - client.on("event", function(event) { + client.on(ClientEvent.Event, function(event) { expect(expectedEventTypes.indexOf(event.getType())).not.toEqual( -1, ); @@ -125,7 +125,7 @@ describe("MatrixClient opts", function() { }); describe("without opts.scheduler", function() { - let client; + let client: MatrixClient; beforeEach(function() { client = new MatrixClient({ fetchFn: httpBackend.fetchFn as typeof global.fetch, diff --git a/spec/integ/matrix-client-room-timeline.spec.ts b/spec/integ/matrix-client-room-timeline.spec.ts index f89ab04e2b8..3c2aa77a6e3 100644 --- a/spec/integ/matrix-client-room-timeline.spec.ts +++ b/spec/integ/matrix-client-room-timeline.spec.ts @@ -18,7 +18,16 @@ import HttpBackend from "matrix-mock-request"; import * as utils from "../test-utils/test-utils"; import { EventStatus } from "../../src/models/event"; -import { MatrixError, ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src"; +import { + MatrixError, + ClientEvent, + IEvent, + MatrixClient, + RoomEvent, + ISyncResponse, + IMinimalEvent, + IRoomEvent, Room, +} from "../../src"; import { TestClient } from "../TestClient"; describe("MatrixClient room timelines", function() { @@ -39,7 +48,7 @@ describe("MatrixClient room timelines", function() { name: "Old room name", }, }); - let NEXT_SYNC_DATA; + let NEXT_SYNC_DATA: Partial; const SYNC_DATA = { next_batch: "s_5_3", rooms: { @@ -88,7 +97,7 @@ describe("MatrixClient room timelines", function() { }, }, leave: {}, - }, + } as unknown as ISyncResponse["rooms"], }; events.forEach(function(e) { if (e.room_id !== roomId) { @@ -96,11 +105,11 @@ describe("MatrixClient room timelines", function() { } if (e.state_key) { // push the current - NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e); + NEXT_SYNC_DATA.rooms!.join[roomId].timeline.events.push(e as unknown as IRoomEvent); } else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) { - NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e); + NEXT_SYNC_DATA.rooms!.join[roomId].ephemeral.events.push(e as unknown as IMinimalEvent); } else { - NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e); + NEXT_SYNC_DATA.rooms!.join[roomId].timeline.events.push(e as unknown as IRoomEvent); } }); } @@ -237,7 +246,7 @@ describe("MatrixClient room timelines", function() { }); describe("paginated events", function() { - let sbEvents; + let sbEvents: Partial[]; const sbEndTok = "pagin_end"; beforeEach(function() { @@ -559,7 +568,7 @@ describe("MatrixClient room timelines", function() { utils.mkMessage({ user: userId, room: roomId }), ]; setNextSyncData(eventData); - NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true; + NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true; return Promise.all([ httpBackend!.flush("/versions", 1), @@ -593,7 +602,7 @@ describe("MatrixClient room timelines", function() { utils.mkMessage({ user: userId, room: roomId }), ]; setNextSyncData(eventData); - NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true; + NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true; return Promise.all([ httpBackend!.flush("/sync", 1), @@ -638,7 +647,7 @@ describe("MatrixClient room timelines", function() { end: "end_token", }; - let room; + let room: Room; beforeEach(async () => { setNextSyncData(initialSyncEventData); diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index 8c3969bee7d..dbb891449cc 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -33,7 +33,7 @@ import { IJoinedRoom, IStateEvent, IMinimalEvent, - NotificationCountType, + NotificationCountType, IEphemeral, } from "../../src"; import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync'; import * as utils from "../test-utils/test-utils"; @@ -524,105 +524,101 @@ describe("MatrixClient syncing", () => { const syncData = { rooms: { join: { - - }, - }, - }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "hello", - }), - ], - }, - state: { - events: [ - utils.mkEvent({ - type: "m.room.name", room: roomOne, user: otherUserId, - content: { - name: "Old room name", + [roomOne]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello", + }), + ], }, - }), - utils.mkMembership({ - room: roomOne, mship: "join", user: otherUserId, - }), - utils.mkMembership({ - room: roomOne, mship: "join", user: selfUserId, - }), - utils.mkEvent({ - type: "m.room.create", room: roomOne, user: selfUserId, - content: { - creator: selfUserId, + state: { + events: [ + utils.mkEvent({ + type: "m.room.name", room: roomOne, user: otherUserId, + content: { + name: "Old room name", + }, + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: otherUserId, + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: selfUserId, + }), + utils.mkEvent({ + type: "m.room.create", room: roomOne, user: selfUserId, + content: { + creator: selfUserId, + }, + }), + ], }, - }), - ], - }, - }; - syncData.rooms.join[roomTwo] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomTwo, user: otherUserId, msg: "hiii", - }), - ], - }, - state: { - events: [ - utils.mkMembership({ - room: roomTwo, mship: "join", user: otherUserId, - name: otherDisplayName, - }), - utils.mkMembership({ - room: roomTwo, mship: "join", user: selfUserId, - }), - utils.mkEvent({ - type: "m.room.create", room: roomTwo, user: selfUserId, - content: { - creator: selfUserId, + }, + [roomTwo]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomTwo, user: otherUserId, msg: "hiii", + }), + ], }, - }), - ], + state: { + events: [ + utils.mkMembership({ + room: roomTwo, mship: "join", user: otherUserId, + name: otherDisplayName, + }), + utils.mkMembership({ + room: roomTwo, mship: "join", user: selfUserId, + }), + utils.mkEvent({ + type: "m.room.create", room: roomTwo, user: selfUserId, + content: { + creator: selfUserId, + }, + }), + ], + }, + }, + }, }, }; const nextSyncData = { rooms: { join: { - + [roomOne]: { + state: { + events: [ + utils.mkEvent({ + type: "m.room.name", room: roomOne, user: selfUserId, + content: { name: "A new room name" }, + }), + ], + }, + }, + [roomTwo]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomTwo, user: otherUserId, msg: msgText, + }), + ], + }, + ephemeral: { + events: [ + utils.mkEvent({ + type: "m.typing", room: roomTwo, + content: { user_ids: [otherUserId] }, + }), + ], + }, + }, }, }, }; - nextSyncData.rooms.join[roomOne] = { - state: { - events: [ - utils.mkEvent({ - type: "m.room.name", room: roomOne, user: selfUserId, - content: { name: "A new room name" }, - }), - ], - }, - }; - - nextSyncData.rooms.join[roomTwo] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomTwo, user: otherUserId, msg: msgText, - }), - ], - }, - ephemeral: { - events: [ - utils.mkEvent({ - type: "m.typing", room: roomTwo, - content: { user_ids: [otherUserId] }, - }), - ], - }, - }; - it("should continually recalculate the right room name.", () => { httpBackend!.when("GET", "/sync").respond(200, syncData); httpBackend!.when("GET", "/sync").respond(200, nextSyncData); @@ -635,9 +631,7 @@ describe("MatrixClient syncing", () => { ]).then(() => { const room = client!.getRoom(roomOne)!; // should have clobbered the name to the one from /events - expect(room.name).toEqual( - nextSyncData.rooms.join[roomOne].state.events[0].content.name, - ); + expect(room.name).toEqual(nextSyncData.rooms.join[roomOne].state.events[0].content?.name); }); }); @@ -742,46 +736,48 @@ describe("MatrixClient syncing", () => { const normalFirstSync = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - normalFirstSync.rooms.join[roomOne] = { - timeline: { - events: [normalMessageEvent], - prev_batch: "pagTok", - }, - state: { - events: [roomCreateEvent], + join: { + [roomOne]: { + timeline: { + events: [normalMessageEvent], + prev_batch: "pagTok", + }, + state: { + events: [roomCreateEvent], + }, + }, + }, }, }; const nextSyncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - nextSyncData.rooms.join[roomOne] = { - timeline: { - events: [ - // In subsequent syncs, a marker event in timeline - // range should normally trigger - // `timelineNeedsRefresh=true` but this marker isn't - // being sent by the room creator so it has no - // special meaning in existing room versions. - utils.mkEvent({ - type: UNSTABLE_MSC2716_MARKER.name, - room: roomOne, - // The important part we're testing is here! - // `userC` is not the room creator. - user: userC, - skey: "", - content: { - "m.insertion_id": "$abc", + join: { + [roomOne]: { + timeline: { + events: [ + // In subsequent syncs, a marker event in timeline + // range should normally trigger + // `timelineNeedsRefresh=true` but this marker isn't + // being sent by the room creator so it has no + // special meaning in existing room versions. + utils.mkEvent({ + type: UNSTABLE_MSC2716_MARKER.name, + room: roomOne, + // The important part we're testing is here! + // `userC` is not the room creator. + user: userC, + skey: "", + content: { + "m.insertion_id": "$abc", + }, + }), + ], + prev_batch: "pagTok", }, - }), - ], - prev_batch: "pagTok", + }, + }, }, }; @@ -831,16 +827,17 @@ describe("MatrixClient syncing", () => { const normalFirstSync = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - normalFirstSync.rooms.join[roomOne] = { - timeline: { - events: [normalMessageEvent], - prev_batch: "pagTok", - }, - state: { - events: [roomCreateEvent], + join: { + [roomOne]: { + timeline: { + events: [normalMessageEvent], + prev_batch: "pagTok", + }, + state: { + events: [roomCreateEvent], + }, + }, + }, }, }; @@ -849,16 +846,17 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [normalMessageEvent], - prev_batch: "pagTok", - }, - state: { - events: [roomCreateEvent], + join: { + [roomOne]: { + timeline: { + events: [normalMessageEvent], + prev_batch: "pagTok", + }, + state: { + events: [roomCreateEvent], + }, + }, + }, }, }; @@ -879,16 +877,17 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [markerEventFromRoomCreator], - prev_batch: "pagTok", - }, - state: { - events: [roomCreateEvent], + join: { + [roomOne]: { + timeline: { + events: [markerEventFromRoomCreator], + prev_batch: "pagTok", + }, + state: { + events: [roomCreateEvent], + }, + }, + }, }, }; @@ -909,19 +908,20 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [normalMessageEvent], - prev_batch: "pagTok", - }, - state: { - events: [ - roomCreateEvent, - markerEventFromRoomCreator, - ], + join: { + [roomOne]: { + timeline: { + events: [normalMessageEvent], + prev_batch: "pagTok", + }, + state: { + events: [ + roomCreateEvent, + markerEventFromRoomCreator, + ], + }, + }, + }, }, }; @@ -942,17 +942,18 @@ describe("MatrixClient syncing", () => { const nextSyncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - nextSyncData.rooms.join[roomOne] = { - timeline: { - events: [ - // In subsequent syncs, a marker event in timeline - // range should trigger `timelineNeedsRefresh=true` - markerEventFromRoomCreator, - ], - prev_batch: "pagTok", + join: { + [roomOne]: { + timeline: { + events: [ + // In subsequent syncs, a marker event in timeline + // range should trigger `timelineNeedsRefresh=true` + markerEventFromRoomCreator, + ], + prev_batch: "pagTok", + }, + }, + }, }, }; @@ -993,24 +994,25 @@ describe("MatrixClient syncing", () => { const nextSyncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - nextSyncData.rooms.join[roomOne] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "hello again", - }), - ], - prev_batch: "pagTok", - }, - state: { - events: [ - // In subsequent syncs, a marker event in state - // should trigger `timelineNeedsRefresh=true` - markerEventFromRoomCreator, - ], + join: { + [roomOne]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello again", + }), + ], + prev_batch: "pagTok", + }, + state: { + events: [ + // In subsequent syncs, a marker event in state + // should trigger `timelineNeedsRefresh=true` + markerEventFromRoomCreator, + ], + }, + }, + }, }, }; @@ -1095,19 +1097,20 @@ describe("MatrixClient syncing", () => { const limitedSyncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - limitedSyncData.rooms.join[roomOne] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "world", - }), - ], - // The important part, make the sync `limited` - limited: true, - prev_batch: "newerTok", + join: { + [roomOne]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "world", + }), + ], + // The important part, make the sync `limited` + limited: true, + prev_batch: "newerTok", + }, + }, + }, }, }; httpBackend!.when("GET", "/sync").respond(200, limitedSyncData); @@ -1167,7 +1170,7 @@ describe("MatrixClient syncing", () => { const eventsInRoom = syncData.rooms.join[roomOne].timeline.events; const contextUrl = `/rooms/${encodeURIComponent(roomOne)}/context/` + - `${encodeURIComponent(eventsInRoom[0].event_id)}`; + `${encodeURIComponent(eventsInRoom[0].event_id!)}`; httpBackend!.when("GET", contextUrl) .respond(200, () => { return { @@ -1202,17 +1205,18 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "hello", - }), - ], - prev_batch: "pagTok", + join: { + [roomOne]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello", + }), + ], + prev_batch: "pagTok", + }, + }, + }, }, }; @@ -1229,17 +1233,18 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomTwo] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomTwo, user: otherUserId, msg: "roomtwo", - }), - ], - prev_batch: "roomtwotok", + join: { + [roomTwo]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomTwo, user: otherUserId, msg: "roomtwo", + }), + ], + prev_batch: "roomtwotok", + }, + }, + }, }, }; @@ -1261,18 +1266,19 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "world", - }), - ], - limited: true, - prev_batch: "newerTok", + join: { + [roomOne]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "world", + }), + ], + limited: true, + prev_batch: "newerTok", + }, + }, + }, }, }; httpBackend!.when("GET", "/sync").respond(200, syncData); @@ -1304,42 +1310,44 @@ describe("MatrixClient syncing", () => { const syncData = { rooms: { join: { - - }, - }, - }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "hello", - }), - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "world", - }), - ], - }, - state: { - events: [ - utils.mkEvent({ - type: "m.room.name", room: roomOne, user: otherUserId, - content: { - name: "Old room name", - }, - }), - utils.mkMembership({ - room: roomOne, mship: "join", user: otherUserId, - }), - utils.mkMembership({ - room: roomOne, mship: "join", user: selfUserId, - }), - utils.mkEvent({ - type: "m.room.create", room: roomOne, user: selfUserId, - content: { - creator: selfUserId, + [roomOne]: { + ephemeral: { + events: [], + } as IEphemeral, + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello", + }), + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "world", + }), + ], }, - }), - ], + state: { + events: [ + utils.mkEvent({ + type: "m.room.name", room: roomOne, user: otherUserId, + content: { + name: "Old room name", + }, + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: otherUserId, + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: selfUserId, + }), + utils.mkEvent({ + type: "m.room.create", room: roomOne, user: selfUserId, + content: { + creator: selfUserId, + }, + }), + ], + } as Partial, + }, + }, }, }; @@ -1351,16 +1359,15 @@ describe("MatrixClient syncing", () => { it("should sync receipts from /sync.", () => { const ackEvent = syncData.rooms.join[roomOne].timeline.events[0]; - const receipt = {}; - receipt[ackEvent.event_id] = { + const receipt: Record = {}; + receipt[ackEvent.event_id!] = { "m.read": {}, }; - receipt[ackEvent.event_id]["m.read"][userC] = { + receipt[ackEvent.event_id!]["m.read"][userC] = { ts: 176592842636, }; syncData.rooms.join[roomOne].ephemeral.events = [{ content: receipt, - room_id: roomOne, type: "m.receipt", }]; httpBackend!.when("GET", "/sync").respond(200, syncData); @@ -1425,7 +1432,7 @@ describe("MatrixClient syncing", () => { }, }, }, - }; + } as unknown as ISyncResponse; it("should sync unread notifications.", () => { syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = { [THREAD_ID]: { @@ -1509,18 +1516,18 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - leave: {}, - }, - }; - - syncData.rooms.leave[roomTwo] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomTwo, user: otherUserId, msg: "hello", - }), - ], - prev_batch: "pagTok", + leave: { + [roomTwo]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomTwo, user: otherUserId, msg: "hello", + }), + ], + prev_batch: "pagTok", + }, + }, + }, }, }; diff --git a/spec/integ/megolm-backup.spec.ts b/spec/integ/megolm-backup.spec.ts index 492e4f1dc13..9341d651189 100644 --- a/spec/integ/megolm-backup.spec.ts +++ b/spec/integ/megolm-backup.spec.ts @@ -126,12 +126,13 @@ describe("megolm key backups", function() { const syncResponse = { next_batch: 1, rooms: { - join: {}, - }, - }; - syncResponse.rooms.join[ROOM_ID] = { - timeline: { - events: [ENCRYPTED_EVENT], + join: { + [ROOM_ID]: { + timeline: { + events: [ENCRYPTED_EVENT], + }, + }, + }, }, }; diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 7a37f572bba..20dfa6141fa 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -119,13 +119,13 @@ describe("SlidingSyncSdk", () => { }; // find an extension on a SlidingSyncSdk instance - const findExtension = (name: string): Extension => { + const findExtension = (name: string): Extension => { expect(mockSlidingSync!.registerExtension).toHaveBeenCalled(); const mockFn = mockSlidingSync!.registerExtension as jest.Mock; // find the extension for (let i = 0; i < mockFn.mock.calls.length; i++) { - const calledExtension = mockFn.mock.calls[i][0] as Extension; - if (calledExtension && calledExtension.name() === name) { + const calledExtension = mockFn.mock.calls[i][0] as Extension; + if (calledExtension?.name() === name) { return calledExtension; } } @@ -581,7 +581,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionE2EE", () => { - let ext: Extension; + let ext: Extension; beforeAll(async () => { await setupClient({ @@ -647,7 +647,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionAccountData", () => { - let ext: Extension; + let ext: Extension; beforeAll(async () => { await setupClient(); @@ -773,7 +773,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionToDevice", () => { - let ext: Extension; + let ext: Extension; beforeAll(async () => { await setupClient(); @@ -871,7 +871,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionTyping", () => { - let ext: Extension; + let ext: Extension; beforeAll(async () => { await setupClient(); @@ -970,7 +970,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionReceipts", () => { - let ext: Extension; + let ext: Extension; const generateReceiptResponse = ( userId: string, roomId: string, eventId: string, recType: string, ts: number, diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 0b1e9fedd4b..c2a4b8e98fc 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -18,7 +18,15 @@ limitations under the License. import EventEmitter from "events"; import MockHttpBackend from "matrix-mock-request"; -import { SlidingSync, SlidingSyncState, ExtensionState, SlidingSyncEvent } from "../../src/sliding-sync"; +import { + SlidingSync, + SlidingSyncState, + ExtensionState, + SlidingSyncEvent, + Extension, + SlidingSyncEventHandlerMap, + MSC3575RoomData, +} from "../../src/sliding-sync"; import { TestClient } from "../TestClient"; import { logger } from "../../src/logger"; import { MatrixClient } from "../../src"; @@ -94,7 +102,7 @@ describe("SlidingSync", () => { is_dm: true, }, }; - const ext = { + const ext: Extension = { name: () => "custom_extension", onRequest: (initial) => { return { initial: initial }; }, onResponse: (res) => { return {}; }, @@ -107,7 +115,7 @@ describe("SlidingSync", () => { slidingSync.start(); // expect everything to be sent - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); @@ -390,8 +398,8 @@ describe("SlidingSync", () => { }], rooms: rooms, }); - const listenerData = {}; - const dataListener = (roomId, roomData) => { + const listenerData: Record = {}; + const dataListener: SlidingSyncEventHandlerMap[SlidingSyncEvent.RoomData] = (roomId, roomData) => { expect(listenerData[roomId]).toBeFalsy(); listenerData[roomId] = roomData; }; @@ -912,7 +920,7 @@ describe("SlidingSync", () => { slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1); // modification before SlidingSync.start() const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId])); - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); @@ -944,7 +952,7 @@ describe("SlidingSync", () => { ranges: [[0, 20]], }; const promise = slidingSync.setList(0, newList); - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); @@ -966,7 +974,7 @@ describe("SlidingSync", () => { }); it("should resolve setListRanges during a connection", async () => { const promise = slidingSync.setListRanges(0, [[20, 40]]); - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); @@ -992,7 +1000,7 @@ describe("SlidingSync", () => { const promise = slidingSync.modifyRoomSubscriptionInfo({ timeline_limit: 99, }); - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); @@ -1016,7 +1024,7 @@ describe("SlidingSync", () => { it("should reject earlier pending promises if a later transaction is acknowledged", async () => { // i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected. const gotTxnIds: any[] = []; - const pushTxn = function(req) { + const pushTxn = function(req: MockHttpBackend["requests"][0]) { gotTxnIds.push(req.data.txn_id); }; const failPromise = slidingSync.setListRanges(0, [[20, 40]]); @@ -1032,7 +1040,7 @@ describe("SlidingSync", () => { expect(failPromise2).rejects.toEqual(gotTxnIds[1]); const okPromise = slidingSync.setListRanges(0, [[0, 20]]); - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check((req) => { txnId = req.data.txn_id; }).respond(200, () => { @@ -1050,7 +1058,7 @@ describe("SlidingSync", () => { it("should not reject later pending promises if an earlier transaction is acknowledged", async () => { // i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should. const gotTxnIds: any[] = []; - const pushTxn = function(req) { + const pushTxn = function(req: MockHttpBackend["requests"][0]) { gotTxnIds.push(req.data?.txn_id); }; const A = slidingSync.setListRanges(0, [[20, 40]]); @@ -1087,7 +1095,7 @@ describe("SlidingSync", () => { promise.finally(() => { pending = false; }); - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); @@ -1275,21 +1283,21 @@ describe("SlidingSync", () => { // Pre-extensions get called BEFORE processing the sync response const preExtName = "foobar"; - let onPreExtensionRequest; - let onPreExtensionResponse; + let onPreExtensionRequest: Extension["onRequest"]; + let onPreExtensionResponse: Extension["onResponse"]; // Post-extensions get called AFTER processing the sync response const postExtName = "foobar2"; - let onPostExtensionRequest; - let onPostExtensionResponse; + let onPostExtensionRequest: Extension["onRequest"]; + let onPostExtensionResponse: Extension["onResponse"]; - const extPre = { + const extPre: Extension = { name: () => preExtName, onRequest: (initial) => { return onPreExtensionRequest(initial); }, onResponse: (res) => { return onPreExtensionResponse(res); }, when: () => ExtensionState.PreProcess, }; - const extPost = { + const extPost: Extension = { name: () => postExtName, onRequest: (initial) => { return onPostExtensionRequest(initial); }, onResponse: (res) => { return onPostExtensionResponse(res); }, @@ -1421,7 +1429,7 @@ describe("SlidingSync", () => { }); function timeout(delayMs: number, reason: string): { promise: Promise, cancel: () => void } { - let timeoutId; + let timeoutId: NodeJS.Timeout; return { promise: new Promise((resolve, reject) => { timeoutId = setTimeout(() => { @@ -1454,7 +1462,7 @@ function listenUntil( const trace = new Error().stack?.split(`\n`)[2]; const t = timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace); return Promise.race([new Promise((resolve, reject) => { - const wrapper = (...args) => { + const wrapper = (...args: any[]) => { try { const data = callback(...args); if (data) { diff --git a/spec/test-utils/test-utils.ts b/spec/test-utils/test-utils.ts index 21ae4d18bad..3dd9c037844 100644 --- a/spec/test-utils/test-utils.ts +++ b/spec/test-utils/test-utils.ts @@ -332,7 +332,7 @@ export function mkReplyMessage( * * @constructor */ -export class MockStorageApi { +export class MockStorageApi implements Storage { private data: Record = {}; public get length() { @@ -354,6 +354,10 @@ export class MockStorageApi { public removeItem(k: string): void { delete this.data[k]; } + + public clear(): void { + this.data = {}; + } } /** diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index b319a5f26ad..a4e1ec374e0 100644 --- a/spec/unit/crypto.spec.ts +++ b/spec/unit/crypto.spec.ts @@ -3,7 +3,7 @@ import '../olm-loader'; import { EventEmitter } from "events"; import type { PkDecryption, PkSigning } from "@matrix-org/olm"; -import { MatrixClient } from "../../src/client"; +import { IClaimOTKsResult, MatrixClient } from "../../src/client"; import { Crypto } from "../../src/crypto"; import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store"; import { MockStorageApi } from "../MockStorageApi"; @@ -23,16 +23,16 @@ import { IRoomEncryption, RoomList } from "../../src/crypto/RoomList"; const Olm = global.Olm; -function awaitEvent(emitter, event) { - return new Promise((resolve, reject) => { +function awaitEvent(emitter: EventEmitter, event: string): Promise { + return new Promise((resolve) => { emitter.once(event, (result) => { resolve(result); }); }); } -async function keyshareEventForEvent(client, event, index): Promise { - const roomId = event.getRoomId(); +async function keyshareEventForEvent(client: MatrixClient, event: MatrixEvent, index?: number): Promise { + const roomId = event.getRoomId()!; const eventContent = event.getWireContent(); const key = await client.crypto!.olmDevice.getInboundGroupSessionKey( roomId, @@ -42,16 +42,16 @@ async function keyshareEventForEvent(client, event, index): Promise ); const ksEvent = new MatrixEvent({ type: "m.forwarded_room_key", - sender: client.getUserId(), + sender: client.getUserId()!, content: { "algorithm": olmlib.MEGOLM_ALGORITHM, "room_id": roomId, "sender_key": eventContent.sender_key, - "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key, + "sender_claimed_ed25519_key": key?.sender_claimed_ed25519_key, "session_id": eventContent.session_id, - "session_key": key.key, - "chain_index": key.chain_index, - "forwarding_curve25519_key_chain": key.forwarding_curve_key_chain, + "session_key": key?.key, + "chain_index": key?.chain_index, + "forwarding_curve25519_key_chain": key?.forwarding_curve25519_key_chain, "org.matrix.msc3061.shared_history": true, }, }); @@ -172,7 +172,8 @@ describe("Crypto", function() { }); describe('Session management', function() { - const otkResponse = { + const otkResponse: IClaimOTKsResult = { + failures: {}, one_time_keys: { '@alice:home.server': { aliceDevice: { @@ -188,11 +189,12 @@ describe("Crypto", function() { }, }, }; - let crypto; - let mockBaseApis; - let mockRoomList; - let fakeEmitter; + let crypto: Crypto; + let mockBaseApis: MatrixClient; + let mockRoomList: RoomList; + + let fakeEmitter: EventEmitter; beforeEach(async function() { const mockStorage = new MockStorageApi() as unknown as Storage; @@ -219,8 +221,8 @@ describe("Crypto", function() { sendToDevice: jest.fn(), getKeyBackupVersion: jest.fn(), isGuest: jest.fn(), - }; - mockRoomList = {}; + } as unknown as MatrixClient; + mockRoomList = {} as unknown as RoomList; fakeEmitter = new EventEmitter(); @@ -233,7 +235,7 @@ describe("Crypto", function() { mockRoomList, [], ); - crypto.registerEventHandlers(fakeEmitter); + crypto.registerEventHandlers(fakeEmitter as any); await crypto.init(); }); @@ -245,7 +247,7 @@ describe("Crypto", function() { const prom = new Promise((resolve) => { mockBaseApis.claimOneTimeKeys = function() { resolve(); - return otkResponse; + return Promise.resolve(otkResponse); }; }); @@ -989,7 +991,7 @@ describe("Crypto", function() { ensureOlmSessionsForDevices.mockResolvedValue({}); encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice"); encryptMessageForDevice.mockImplementation(async (...[result,,,,,, payload]) => { - result.plaintext = JSON.stringify(payload); + result.plaintext = { type: 0, body: JSON.stringify(payload) }; }); client = new TestClient("@alice:example.org", "aliceweb"); @@ -998,7 +1000,7 @@ describe("Crypto", function() { encryptedPayload = { algorithm: "m.olm.v1.curve25519-aes-sha2", sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key, - ciphertext: { plaintext: JSON.stringify(payload) }, + ciphertext: { plaintext: { type: 0, body: JSON.stringify(payload) } }, }; }); @@ -1046,7 +1048,7 @@ describe("Crypto", function() { encryptMessageForDevice.mockImplementation(async (...[result,,,, userId, device, payload]) => { // Refuse to encrypt to Carol's desktop device if (userId === "@carol:example.org" && device.deviceId === "caroldesktop") return; - result.plaintext = JSON.stringify(payload); + result.plaintext = { type: 0, body: JSON.stringify(payload) }; }); client.httpBackend diff --git a/spec/unit/crypto/CrossSigningInfo.spec.ts b/spec/unit/crypto/CrossSigningInfo.spec.ts index f6b64cac440..ad02fb217e7 100644 --- a/spec/unit/crypto/CrossSigningInfo.spec.ts +++ b/spec/unit/crypto/CrossSigningInfo.spec.ts @@ -232,7 +232,7 @@ describe.each([ return store; }], ])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) { - let store; + let store: IndexedDBCryptoStore; beforeAll(() => { store = dbFactory(); diff --git a/spec/unit/crypto/DeviceList.spec.ts b/spec/unit/crypto/DeviceList.spec.ts index 448c92b28e2..38c0e2df89a 100644 --- a/spec/unit/crypto/DeviceList.spec.ts +++ b/spec/unit/crypto/DeviceList.spec.ts @@ -22,6 +22,7 @@ import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store import { DeviceList } from "../../../src/crypto/DeviceList"; import { IDownloadKeyResult, MatrixClient } from "../../../src"; import { OlmDevice } from "../../../src/crypto/OlmDevice"; +import { CryptoStore } from "../../../src/crypto/store/base"; const signedDeviceList: IDownloadKeyResult = { "failures": {}, @@ -88,8 +89,8 @@ const signedDeviceList2: IDownloadKeyResult = { }; describe('DeviceList', function() { - let downloadSpy; - let cryptoStore; + let downloadSpy: jest.Mock; + let cryptoStore: CryptoStore; let deviceLists: DeviceList[] = []; beforeEach(function() { @@ -112,7 +113,7 @@ describe('DeviceList', function() { deviceId: 'HGKAWHRVJQ', } as unknown as MatrixClient; const mockOlm = { - verifySignature: function(key, message, signature) {}, + verifySignature: function(key: string, message: string, signature: string) {}, } as unknown as OlmDevice; const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize); deviceLists.push(dl); diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index b631201996a..48bdbc4ad0c 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -17,6 +17,7 @@ limitations under the License. import { mocked, MockedObject } from 'jest-mock'; import '../../../olm-loader'; +import type { OutboundGroupSession } from "@matrix-org/olm"; import * as algorithms from "../../../../src/crypto/algorithms"; import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store"; import * as testUtils from "../../../test-utils/test-utils"; @@ -31,6 +32,7 @@ import { TypedEventEmitter } from '../../../../src/models/typed-event-emitter'; import { ClientEvent, MatrixClient, RoomMember } from '../../../../src'; import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo'; import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning'; +import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm"; const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!; const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!; @@ -87,7 +89,7 @@ describe("MegolmDecryption", function() { }); describe('receives some keys:', function() { - let groupSession; + let groupSession: OutboundGroupSession; beforeEach(async function() { groupSession = new global.Olm.OutboundGroupSession(); groupSession.create(); @@ -298,10 +300,10 @@ describe("MegolmDecryption", function() { describe("session reuse and key reshares", () => { const rotationPeriodMs = 999 * 24 * 60 * 60 * 1000; // 999 days, so we don't have to deal with it - let megolmEncryption; - let aliceDeviceInfo; - let mockRoom; - let olmDevice; + let megolmEncryption: MegolmEncryptionClass; + let aliceDeviceInfo: DeviceInfo; + let mockRoom: Room; + let olmDevice: OlmDevice; beforeEach(async () => { // @ts-ignore assigning to readonly prop @@ -342,7 +344,7 @@ describe("MegolmDecryption", function() { 'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE', ), getFingerprint: jest.fn().mockReturnValue(''), - }; + } as unknown as DeviceInfo; mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({ '@alice:home.server': { @@ -365,7 +367,7 @@ describe("MegolmDecryption", function() { algorithm: 'm.megolm.v1.aes-sha2', rotation_period_ms: rotationPeriodMs, }, - }); + }) as MegolmEncryptionClass; // Splice the real method onto the mock object as megolm uses this method // on the crypto class in order to encrypt / start sessions @@ -381,7 +383,7 @@ describe("MegolmDecryption", function() { [{ userId: "@alice:home.server" }], ), getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false), - }; + } as unknown as Room; }); it("should use larger otkTimeout when preparing to encrypt room", async () => { @@ -397,11 +399,14 @@ describe("MegolmDecryption", function() { }); it("should generate a new session if this one needs rotation", async () => { + // @ts-ignore - private method access const session = await megolmEncryption.prepareNewSession(false); session.creationTime -= rotationPeriodMs + 10000; // a smidge over the rotation time // Inject expired session which needs rotation + // @ts-ignore - private field access megolmEncryption.setupPromise = Promise.resolve(session); + // @ts-ignore - private method access const prepareNewSessionSpy = jest.spyOn(megolmEncryption, "prepareNewSession"); await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", { body: "Some text", @@ -446,8 +451,8 @@ describe("MegolmDecryption", function() { }); mockBaseApis.sendToDevice.mockClear(); - await megolmEncryption.reshareKeyWithDevice( - olmDevice.deviceCurve25519Key, + await megolmEncryption.reshareKeyWithDevice!( + olmDevice.deviceCurve25519Key!, ct1.session_id, '@alice:home.server', aliceDeviceInfo, @@ -466,8 +471,8 @@ describe("MegolmDecryption", function() { ); mockBaseApis.queueToDevice.mockClear(); - await megolmEncryption.reshareKeyWithDevice( - olmDevice.deviceCurve25519Key, + await megolmEncryption.reshareKeyWithDevice!( + olmDevice.deviceCurve25519Key!, ct1.session_id, '@alice:home.server', aliceDeviceInfo, diff --git a/spec/unit/crypto/algorithms/olm.spec.ts b/spec/unit/crypto/algorithms/olm.spec.ts index b24532091ae..932fb8016b4 100644 --- a/spec/unit/crypto/algorithms/olm.spec.ts +++ b/spec/unit/crypto/algorithms/olm.spec.ts @@ -31,17 +31,21 @@ function makeOlmDevice() { return olmDevice; } -async function setupSession(initiator, opponent) { +async function setupSession(initiator: OlmDevice, opponent: OlmDevice) { await opponent.generateOneTimeKeys(1); const keys = await opponent.getOneTimeKeys(); const firstKey = Object.values(keys['curve25519'])[0]; - const sid = await initiator.createOutboundSession( - opponent.deviceCurve25519Key, firstKey, - ); + const sid = await initiator.createOutboundSession(opponent.deviceCurve25519Key!, firstKey); return sid; } +function alwaysSucceed(promise: Promise): Promise { + // swallow any exception thrown by a promise, so that + // Promise.all doesn't abort + return promise.catch(() => {}); +} + describe("OlmDevice", function() { if (!global.Olm) { logger.warn('Not running megolm unit tests: libolm not present'); @@ -159,11 +163,6 @@ describe("OlmDevice", function() { }, "ABCDEFG"), ], }; - function alwaysSucceed(promise) { - // swallow any exception thrown by a promise, so that - // Promise.all doesn't abort - return promise.catch(() => {}); - } // start two tasks that try to ensure that there's an olm session const promises = Promise.all([ @@ -235,12 +234,6 @@ describe("OlmDevice", function() { ], }; - function alwaysSucceed(promise) { - // swallow any exception thrown by a promise, so that - // Promise.all doesn't abort - return promise.catch(() => {}); - } - const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices( aliceOlmDevice, baseApis, devicesByUserAB, )); diff --git a/spec/unit/crypto/backup.spec.ts b/spec/unit/crypto/backup.spec.ts index 0d0820cd3f8..60fddedc733 100644 --- a/spec/unit/crypto/backup.spec.ts +++ b/spec/unit/crypto/backup.spec.ts @@ -15,8 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MockedObject } from "jest-mock"; - import '../../olm-loader'; import { logger } from "../../../src/logger"; import * as olmlib from "../../../src/crypto/olmlib"; @@ -30,7 +28,10 @@ import { Crypto } from "../../../src/crypto"; import { resetCrossSigningKeys } from "./crypto-utils"; import { BackupManager } from "../../../src/crypto/backup"; import { StubStore } from "../../../src/store/stub"; -import { MatrixScheduler } from '../../../src'; +import { IndexedDBCryptoStore, MatrixScheduler } from '../../../src'; +import { CryptoStore } from "../../../src/crypto/store/base"; +import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm"; +import { IKeyBackupInfo } from "../../../src/crypto/keybackup"; const Olm = global.Olm; @@ -102,29 +103,36 @@ const CURVE25519_BACKUP_INFO = { }, }; -const AES256_BACKUP_INFO = { +const AES256_BACKUP_INFO: IKeyBackupInfo = { algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2", version: '1', - auth_data: { - // FIXME: add iv and mac - }, + auth_data: {} as IKeyBackupInfo["auth_data"], }; -const keys = {}; +const keys: Record = {}; -function getCrossSigningKey(type) { - return keys[type]; +function getCrossSigningKey(type: string) { + return Promise.resolve(keys[type]); } -function saveCrossSigningKeys(k) { +function saveCrossSigningKeys(k: Record) { Object.assign(keys, k); } -function makeTestClient(cryptoStore) { - const scheduler = [ - "getQueueForEvent", "queueEvent", "removeEventFromQueue", +function makeTestScheduler(): MatrixScheduler { + return ([ + "getQueueForEvent", + "queueEvent", + "removeEventFromQueue", "setProcessFunction", - ].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject; + ] as const).reduce((r, k) => { + r[k] = jest.fn(); + return r; + }, {} as MatrixScheduler); +} + +function makeTestClient(cryptoStore: CryptoStore) { + const scheduler = makeTestScheduler(); const store = new StubStore(); return new MatrixClient({ @@ -151,36 +159,33 @@ describe("MegolmBackup", function() { return Olm.init(); }); - let olmDevice; - let mockOlmLib; - let mockCrypto; - let cryptoStore; - let megolmDecryption; + let olmDevice: OlmDevice; + let mockOlmLib: typeof olmlib; + let mockCrypto: Crypto; + let cryptoStore: CryptoStore; + let megolmDecryption: MegolmDecryptionClass; beforeEach(async function() { mockCrypto = testUtils.mock(Crypto, 'Crypto'); + // @ts-ignore making mock mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager"); - mockCrypto.backupKey = new Olm.PkEncryption(); - mockCrypto.backupKey.set_recipient_key( - "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", - ); - mockCrypto.backupInfo = CURVE25519_BACKUP_INFO; + mockCrypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO; cryptoStore = new MemoryCryptoStore(); olmDevice = new OlmDevice(cryptoStore); // we stub out the olm encryption bits - mockOlmLib = {}; + mockOlmLib = {} as unknown as typeof olmlib; mockOlmLib.ensureOlmSessionsForDevices = jest.fn(); mockOlmLib.encryptMessageForDevice = jest.fn().mockResolvedValue(undefined); }); describe("backup", function() { - let mockBaseApis; + let mockBaseApis: MatrixClient; beforeEach(function() { - mockBaseApis = {}; + mockBaseApis = {} as unknown as MatrixClient; megolmDecryption = new MegolmDecryption({ userId: '@user:id', @@ -188,8 +193,9 @@ describe("MegolmBackup", function() { olmDevice: olmDevice, baseApis: mockBaseApis, roomId: ROOM_ID, - }); + }) as MegolmDecryptionClass; + // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; // clobber the setTimeout function to run 100x faster. @@ -239,6 +245,7 @@ describe("MegolmBackup", function() { }; mockCrypto.cancelRoomKeyRequest = function() {}; + // @ts-ignore readonly field write mockCrypto.backupManager = { backupGroupSession: jest.fn(), }; @@ -264,21 +271,22 @@ describe("MegolmBackup", function() { olmDevice: olmDevice, baseApis: client, roomId: ROOM_ID, - }); + }) as MegolmDecryptionClass; + // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; return client.initCrypto() .then(() => { return cryptoStore.doTxn( "readwrite", - [cryptoStore.STORE_SESSION], + [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { cryptoStore.addEndToEndInboundGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), { - forwardingCurve25519KeyChain: undefined, + forwardingCurve25519KeyChain: undefined!, keysClaimed: { ed25519: "SENDER_ED25519", }, @@ -298,25 +306,25 @@ describe("MegolmBackup", function() { }); let numCalls = 0; return new Promise((resolve, reject) => { - client.http.authedRequest = function( + client.http.authedRequest = function( method, path, queryParams, data, opts, - ): Promise { + ): any { ++numCalls; expect(numCalls).toBeLessThanOrEqual(1); if (numCalls >= 2) { // exit out of retry loop if there's something wrong reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({} as T); + return Promise.resolve({}); } expect(method).toBe("PUT"); expect(path).toBe("/room_keys/keys"); - expect(queryParams.version).toBe('1'); + expect(queryParams?.version).toBe('1'); expect((data as Record).rooms[ROOM_ID].sessions).toBeDefined(); expect((data as Record).rooms[ROOM_ID].sessions).toHaveProperty( groupSession.session_id(), ); resolve(); - return Promise.resolve({} as T); + return Promise.resolve({}); }; client.crypto!.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", @@ -343,8 +351,9 @@ describe("MegolmBackup", function() { olmDevice: olmDevice, baseApis: client, roomId: ROOM_ID, - }); + }) as MegolmDecryptionClass; + // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; return client.initCrypto() @@ -354,13 +363,13 @@ describe("MegolmBackup", function() { .then(() => { return cryptoStore.doTxn( "readwrite", - [cryptoStore.STORE_SESSION], + [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { cryptoStore.addEndToEndInboundGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), { - forwardingCurve25519KeyChain: undefined, + forwardingCurve25519KeyChain: undefined!, keysClaimed: { ed25519: "SENDER_ED25519", }, @@ -381,25 +390,25 @@ describe("MegolmBackup", function() { }); let numCalls = 0; return new Promise((resolve, reject) => { - client.http.authedRequest = function( + client.http.authedRequest = function( method, path, queryParams, data, opts, - ): Promise { + ): any { ++numCalls; expect(numCalls).toBeLessThanOrEqual(1); if (numCalls >= 2) { // exit out of retry loop if there's something wrong reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({} as T); + return Promise.resolve({}); } expect(method).toBe("PUT"); expect(path).toBe("/room_keys/keys"); - expect(queryParams.version).toBe('1'); + expect(queryParams?.version).toBe('1'); expect((data as Record).rooms[ROOM_ID].sessions).toBeDefined(); expect((data as Record).rooms[ROOM_ID].sessions).toHaveProperty( groupSession.session_id(), ); resolve(); - return Promise.resolve({} as T); + return Promise.resolve({}); }; client.crypto!.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", @@ -426,8 +435,9 @@ describe("MegolmBackup", function() { olmDevice: olmDevice, baseApis: client, roomId: ROOM_ID, - }); + }) as MegolmDecryptionClass; + // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; await client.initCrypto(); @@ -437,10 +447,10 @@ describe("MegolmBackup", function() { let numCalls = 0; await Promise.all([ new Promise((resolve, reject) => { - let backupInfo; + let backupInfo: Record | BodyInit | undefined; client.http.authedRequest = function( method, path, queryParams, data, opts, - ) { + ): any { ++numCalls; expect(numCalls).toBeLessThanOrEqual(2); if (numCalls === 1) { @@ -486,10 +496,7 @@ describe("MegolmBackup", function() { const ibGroupSession = new Olm.InboundGroupSession(); ibGroupSession.create(groupSession.session_key()); - const scheduler = [ - "getQueueForEvent", "queueEvent", "removeEventFromQueue", - "setProcessFunction", - ].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject; + const scheduler = makeTestScheduler(); const store = new StubStore(); const client = new MatrixClient({ baseUrl: "https://my.home.server", @@ -509,20 +516,21 @@ describe("MegolmBackup", function() { olmDevice: olmDevice, baseApis: client, roomId: ROOM_ID, - }); + }) as MegolmDecryptionClass; + // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; await client.initCrypto(); await cryptoStore.doTxn( "readwrite", - [cryptoStore.STORE_SESSION], + [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { cryptoStore.addEndToEndInboundGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), { - forwardingCurve25519KeyChain: undefined, + forwardingCurve25519KeyChain: undefined!, keysClaimed: { ed25519: "SENDER_ED25519", }, @@ -542,26 +550,26 @@ describe("MegolmBackup", function() { let numCalls = 0; await new Promise((resolve, reject) => { - client.http.authedRequest = function( + client.http.authedRequest = function( method, path, queryParams, data, opts, - ): Promise { + ): any { ++numCalls; expect(numCalls).toBeLessThanOrEqual(2); if (numCalls >= 3) { // exit out of retry loop if there's something wrong reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({} as T); + return Promise.resolve({}); } expect(method).toBe("PUT"); expect(path).toBe("/room_keys/keys"); - expect(queryParams.version).toBe('1'); + expect(queryParams?.version).toBe('1'); expect((data as Record).rooms[ROOM_ID].sessions).toBeDefined(); expect((data as Record).rooms[ROOM_ID].sessions).toHaveProperty( groupSession.session_id(), ); if (numCalls > 1) { resolve(); - return Promise.resolve({} as T); + return Promise.resolve({}); } else { return Promise.reject( new Error("this is an expected failure"), @@ -579,7 +587,7 @@ describe("MegolmBackup", function() { }); describe("restore", function() { - let client; + let client: MatrixClient; beforeEach(function() { client = makeTestClient(cryptoStore); @@ -590,8 +598,9 @@ describe("MegolmBackup", function() { olmDevice: olmDevice, baseApis: client, roomId: ROOM_ID, - }); + }) as MegolmDecryptionClass; + // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; return client.initCrypto(); @@ -603,7 +612,7 @@ describe("MegolmBackup", function() { it('can restore from backup (Curve25519 version)', function() { client.http.authedRequest = function() { - return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); + return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); }; return client.restoreKeyBackupWithRecoveryKey( "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", @@ -620,7 +629,7 @@ describe("MegolmBackup", function() { it('can restore from backup (AES-256 version)', function() { client.http.authedRequest = function() { - return Promise.resolve(AES256_KEY_BACKUP_DATA); + return Promise.resolve(AES256_KEY_BACKUP_DATA); }; return client.restoreKeyBackupWithRecoveryKey( "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", @@ -637,7 +646,7 @@ describe("MegolmBackup", function() { it('can restore backup by room (Curve25519 version)', function() { client.http.authedRequest = function() { - return Promise.resolve({ + return Promise.resolve({ rooms: { [ROOM_ID]: { sessions: { @@ -649,7 +658,7 @@ describe("MegolmBackup", function() { }; return client.restoreKeyBackupWithRecoveryKey( "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", - null, null, CURVE25519_BACKUP_INFO, + null!, null!, CURVE25519_BACKUP_INFO, ).then(() => { return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); }).then((res) => { @@ -659,18 +668,18 @@ describe("MegolmBackup", function() { it('has working cache functions', async function() { const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]); - await client.crypto.storeSessionBackupPrivateKey(key); - const result = await client.crypto.getSessionBackupPrivateKey(); - expect(new Uint8Array(result)).toEqual(key); + await client.crypto!.storeSessionBackupPrivateKey(key); + const result = await client.crypto!.getSessionBackupPrivateKey(); + expect(new Uint8Array(result!)).toEqual(key); }); it('caches session backup keys as it encounters them', async function() { - const cachedNull = await client.crypto.getSessionBackupPrivateKey(); + const cachedNull = await client.crypto!.getSessionBackupPrivateKey(); expect(cachedNull).toBeNull(); client.http.authedRequest = function() { - return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); + return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); }; - await new Promise((resolve) => { + await new Promise((resolve) => { client.restoreKeyBackupWithRecoveryKey( "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", ROOM_ID, @@ -679,7 +688,7 @@ describe("MegolmBackup", function() { { cacheCompleteCallback: resolve }, ); }); - const cachedKey = await client.crypto.getSessionBackupPrivateKey(); + const cachedKey = await client.crypto!.getSessionBackupPrivateKey(); expect(cachedKey).not.toBeNull(); }); @@ -688,7 +697,7 @@ describe("MegolmBackup", function() { algorithm: "this.algorithm.does.not.exist", }); client.http.authedRequest = function() { - return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); + return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); }; await expect(client.restoreKeyBackupWithRecoveryKey( @@ -702,10 +711,7 @@ describe("MegolmBackup", function() { describe("flagAllGroupSessionsForBackup", () => { it("should return number of sesions needing backup", async () => { - const scheduler = [ - "getQueueForEvent", "queueEvent", "removeEventFromQueue", - "setProcessFunction", - ].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject; + const scheduler = makeTestScheduler(); const store = new StubStore(); const client = new MatrixClient({ baseUrl: "https://my.home.server", diff --git a/spec/unit/crypto/cross-signing.spec.ts b/spec/unit/crypto/cross-signing.spec.ts index e9c112c5005..fe5b37eb206 100644 --- a/spec/unit/crypto/cross-signing.spec.ts +++ b/spec/unit/crypto/cross-signing.spec.ts @@ -18,23 +18,24 @@ limitations under the License. import '../../olm-loader'; import anotherjson from 'another-json'; import { PkSigning } from '@matrix-org/olm'; +import HttpBackend from "matrix-mock-request"; import * as olmlib from "../../../src/crypto/olmlib"; import { MatrixError } from '../../../src/http-api'; import { logger } from '../../../src/logger'; import { ICrossSigningKey, ICreateClientOpts, ISignedKey } from '../../../src/client'; -import { CryptoEvent } from '../../../src/crypto'; +import { CryptoEvent, IBootstrapCrossSigningOpts } from '../../../src/crypto'; import { IDevice } from '../../../src/crypto/deviceinfo'; import { TestClient } from '../../TestClient'; import { resetCrossSigningKeys } from "./crypto-utils"; -const PUSH_RULES_RESPONSE = { +const PUSH_RULES_RESPONSE: Response = { method: "GET", path: "/pushrules/", data: {}, }; -const filterResponse = function(userId) { +const filterResponse = function(userId: string): Response { const filterPath = "/user/" + encodeURIComponent(userId) + "/filter"; return { method: "POST", @@ -43,7 +44,13 @@ const filterResponse = function(userId) { }; }; -function setHttpResponses(httpBackend, responses) { +interface Response { + method: 'GET' | 'PUT' | 'POST' | 'DELETE'; + path: string; + data: object; +} + +function setHttpResponses(httpBackend: HttpBackend, responses: Response[]) { responses.forEach(response => { httpBackend .when(response.method, response.path) @@ -54,13 +61,13 @@ function setHttpResponses(httpBackend, responses) { async function makeTestClient( userInfo: { userId: string, deviceId: string}, options: Partial = {}, - keys = {}, + keys: Record = {}, ) { - function getCrossSigningKey(type) { - return keys[type]; + function getCrossSigningKey(type: string) { + return keys[type] ?? null; } - function saveCrossSigningKeys(k) { + function saveCrossSigningKeys(k: Record) { Object.assign(keys, k); } @@ -142,7 +149,9 @@ describe("Cross Signing", function() { alice.uploadKeySignatures = async () => ({ failures: {} }); alice.setAccountData = async () => ({}); alice.getAccountDataFromServer = async (): Promise => ({} as T); - const authUploadDeviceSigningKeys = async func => await func({}); + const authUploadDeviceSigningKeys: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async func => { + await func({}); + }; // Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass // through failure, stopping before actually applying changes. @@ -275,7 +284,7 @@ describe("Cross Signing", function() { ); // feed sync result that includes master key, ssk, device key - const responses = [ + const responses: Response[] = [ PUSH_RULES_RESPONSE, { method: "POST", @@ -464,7 +473,7 @@ describe("Cross Signing", function() { }); it.skip("should trust signatures received from other devices", async function() { - const aliceKeys: Record = {}; + const aliceKeys: Record = {}; const { client: alice, httpBackend } = await makeTestClient( { userId: "@alice:example.com", deviceId: "Osborne2" }, undefined, @@ -494,8 +503,7 @@ describe("Cross Signing", function() { }); // @ts-ignore private property - const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"] - .Osborne2; + const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"].Osborne2; const aliceDevice = { user_id: "@alice:example.com", device_id: "Osborne2", @@ -549,7 +557,7 @@ describe("Cross Signing", function() { // - ssk // - master key signed by her usk (pretend that it was signed by another // of Alice's devices) - const responses = [ + const responses: Response[] = [ PUSH_RULES_RESPONSE, { method: "POST", @@ -853,7 +861,7 @@ describe("Cross Signing", function() { }); it("should offer to upgrade device verifications to cross-signing", async function() { - let upgradeResolveFunc; + let upgradeResolveFunc: Function; const { client: alice } = await makeTestClient( { userId: "@alice:example.com", deviceId: "Osborne2" }, diff --git a/spec/unit/crypto/crypto-utils.ts b/spec/unit/crypto/crypto-utils.ts index 1391d79f193..76f2a083575 100644 --- a/spec/unit/crypto/crypto-utils.ts +++ b/spec/unit/crypto/crypto-utils.ts @@ -1,14 +1,16 @@ import { IRecoveryKey } from '../../../src/crypto/api'; import { CrossSigningLevel } from '../../../src/crypto/CrossSigning'; import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store'; +import { MatrixClient } from "../../../src"; +import { CryptoEvent } from "../../../src/crypto"; // needs to be phased out and replaced with bootstrapSecretStorage, // but that is doing too much extra stuff for it to be an easy transition. export async function resetCrossSigningKeys( - client, + client: MatrixClient, { level }: { level?: CrossSigningLevel} = {}, ): Promise { - const crypto = client.crypto; + const crypto = client.crypto!; const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys); try { @@ -28,7 +30,8 @@ export async function resetCrossSigningKeys( crypto.crossSigningInfo.keys = oldKeys; throw e; } - crypto.emit("crossSigning.keysChanged", {}); + crypto.emit(CryptoEvent.KeysChanged, {}); + // @ts-ignore await crypto.afterCrossSigningLocalKeyChange(); } diff --git a/spec/unit/crypto/secrets.spec.ts b/spec/unit/crypto/secrets.spec.ts index 386df0d22b0..885f0f6c37d 100644 --- a/spec/unit/crypto/secrets.spec.ts +++ b/spec/unit/crypto/secrets.spec.ts @@ -16,6 +16,7 @@ limitations under the License. import '../../olm-loader'; import * as olmlib from "../../../src/crypto/olmlib"; +import { IObject } from "../../../src/crypto/olmlib"; import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage"; import { MatrixEvent } from "../../../src/models/event"; import { TestClient } from '../../TestClient'; @@ -23,9 +24,11 @@ import { makeTestClients } from './verification/util'; import { encryptAES } from "../../../src/crypto/aes"; import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils"; import { logger } from '../../../src/logger'; -import { ClientEvent, ICreateClientOpts } from '../../../src/client'; +import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from '../../../src/client'; import { ISecretStorageKeyInfo } from '../../../src/crypto/api'; import { DeviceInfo } from '../../../src/crypto/deviceinfo'; +import { ISignatures } from "../../../src/@types/signed"; +import { ICurve25519AuthData } from "../../../src/crypto/keybackup"; async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial = {}) { const client = (new TestClient( @@ -48,9 +51,15 @@ async function makeTestClient(userInfo: { userId: string, deviceId: string}, opt // Wrapper around pkSign to return a signed object. pkSign returns the // signature, rather than the signed object. -function sign(obj, key, userId) { +function sign(obj: T, key: Uint8Array, userId: string): T & { + signatures: ISignatures; + unsigned?: object; +} { olmlib.pkSign(obj, key, userId, ''); - return obj; + return obj as T & { + signatures: ISignatures; + unsigned?: object; + }; } describe("Secrets", function() { @@ -169,12 +178,12 @@ describe("Secrets", function() { return [newKeyId, key]; }); - let keys = {}; + let keys: Record = {}; const alice = await makeTestClient( { userId: "@alice:example.com", deviceId: "Osborne2" }, { cryptoCallbacks: { - getCrossSigningKey: t => keys[t], + getCrossSigningKey: t => Promise.resolve(keys[t]), saveCrossSigningKeys: k => keys = k, getSecretStorageKey: getKey, }, @@ -227,7 +236,7 @@ describe("Secrets", function() { cryptoCallbacks: { onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => { expect(secretName).toBe("foo"); - return "bar"; + return Promise.resolve("bar"); }, }, }, @@ -354,7 +363,7 @@ describe("Secrets", function() { const storagePublicKey = decryption.generate_key(); const storagePrivateKey = decryption.get_private_key(); - const bob = await makeTestClient( + const bob: MatrixClient = await makeTestClient( { userId: "@bob:example.com", deviceId: "bob1", @@ -364,15 +373,15 @@ describe("Secrets", function() { getSecretStorageKey: async request => { const defaultKeyId = await bob.getDefaultSecretStorageKeyId(); expect(Object.keys(request.keys)).toEqual([defaultKeyId]); - return [defaultKeyId, storagePrivateKey]; + return [defaultKeyId!, storagePrivateKey]; }, }, }, ); - bob.uploadDeviceSigningKeys = async () => {}; - bob.uploadKeySignatures = async () => {}; - bob.setAccountData = async function(eventType, contents, callback) { + bob.uploadDeviceSigningKeys = async () => ({}); + bob.uploadKeySignatures = async () => ({ failures: {} }); + bob.setAccountData = async function(eventType, contents) { const event = new MatrixEvent({ type: eventType, content: contents, @@ -380,16 +389,19 @@ describe("Secrets", function() { this.store.storeAccountDataEvents([ event, ]); - this.emit("accountData", event); + this.emit(ClientEvent.AccountData, event); + return {}; }; - bob.crypto.backupManager.checkKeyBackup = async () => {}; + bob.crypto!.backupManager.checkKeyBackup = async () => null; - const crossSigning = bob.crypto.crossSigningInfo; - const secretStorage = bob.crypto.secretStorage; + const crossSigning = bob.crypto!.crossSigningInfo; + const secretStorage = bob.crypto!.secretStorage; // Set up cross-signing keys from scratch with specific storage key await bob.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async func => await func({}), + authUploadDeviceSigningKeys: async func => { + await func({}); + }, }); await bob.bootstrapSecretStorage({ createSecretStorageKey: async () => ({ @@ -400,13 +412,15 @@ describe("Secrets", function() { }); // Clear local cross-signing keys and read from secret storage - bob.crypto.deviceList.storeCrossSigningForUser( + bob.crypto!.deviceList.storeCrossSigningForUser( "@bob:example.com", crossSigning.toStorage(), ); crossSigning.keys = {}; await bob.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async func => await func({}), + authUploadDeviceSigningKeys: async func => { + await func({}); + }, }); expect(crossSigning.getId()).toBeTruthy(); @@ -422,7 +436,7 @@ describe("Secrets", function() { user_signing: USK, self_signing: SSK, }; - const secretStorageKeys = { + const secretStorageKeys: Record = { key_id: SSSSKey, }; const alice = await makeTestClient( @@ -498,14 +512,14 @@ describe("Secrets", function() { [`ed25519:${XSPubKey}`]: XSPubKey, }, }, - self_signing: sign({ + self_signing: sign({ user_id: "@alice:example.com", usage: ["self_signing"], keys: { [`ed25519:${SSPubKey}`]: SSPubKey, }, }, XSK, "@alice:example.com"), - user_signing: sign({ + user_signing: sign({ user_id: "@alice:example.com", usage: ["user_signing"], keys: { @@ -557,7 +571,7 @@ describe("Secrets", function() { user_signing: USK, self_signing: SSK, }; - const secretStorageKeys = { + const secretStorageKeys: Record = { key_id: SSSSKey, }; const alice = await makeTestClient( @@ -642,14 +656,14 @@ describe("Secrets", function() { [`ed25519:${XSPubKey}`]: XSPubKey, }, }, - self_signing: sign({ + self_signing: sign({ user_id: "@alice:example.com", usage: ["self_signing"], keys: { [`ed25519:${SSPubKey}`]: SSPubKey, }, }, XSK, "@alice:example.com"), - user_signing: sign({ + user_signing: sign({ user_id: "@alice:example.com", usage: ["user_signing"], keys: { diff --git a/spec/unit/crypto/verification/sas.spec.ts b/spec/unit/crypto/verification/sas.spec.ts index ee058c7f0a0..2d05b31b2c3 100644 --- a/spec/unit/crypto/verification/sas.spec.ts +++ b/spec/unit/crypto/verification/sas.spec.ts @@ -18,12 +18,12 @@ import "../../../olm-loader"; import { makeTestClients } from './util'; import { MatrixEvent } from "../../../../src/models/event"; import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS"; -import { DeviceInfo } from "../../../../src/crypto/deviceinfo"; +import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo"; import { CryptoEvent, verificationMethods } from "../../../../src/crypto"; import * as olmlib from "../../../../src/crypto/olmlib"; import { logger } from "../../../../src/logger"; import { resetCrossSigningKeys } from "../crypto-utils"; -import { VerificationBase as Verification, VerificationBase } from "../../../../src/crypto/verification/Base"; +import { VerificationBase } from "../../../../src/crypto/verification/Base"; import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel"; import { MatrixClient } from "../../../../src"; import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest"; @@ -31,8 +31,8 @@ import { TestClient } from "../../../TestClient"; const Olm = global.Olm; -let ALICE_DEVICES; -let BOB_DEVICES; +let ALICE_DEVICES: Record; +let BOB_DEVICES: Record; describe("SAS verification", function() { if (!global.Olm) { @@ -75,7 +75,7 @@ describe("SAS verification", function() { let bob: TestClient; let aliceSasEvent: ISasEvent | null; let bobSasEvent: ISasEvent | null; - let aliceVerifier: Verification; + let aliceVerifier: SAS; let bobPromise: Promise>; let clearTestClientTimeouts: () => void; @@ -95,25 +95,25 @@ describe("SAS verification", function() { ALICE_DEVICES = { Osborne2: { - user_id: "@alice:example.com", - device_id: "Osborne2", algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], keys: { - "ed25519:Osborne2": aliceDevice.deviceEd25519Key, - "curve25519:Osborne2": aliceDevice.deviceCurve25519Key, + "ed25519:Osborne2": aliceDevice.deviceEd25519Key!, + "curve25519:Osborne2": aliceDevice.deviceCurve25519Key!, }, + verified: DeviceInfo.DeviceVerification.UNVERIFIED, + known: false, }, }; BOB_DEVICES = { Dynabook: { - user_id: "@bob:example.com", - device_id: "Dynabook", algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], keys: { - "ed25519:Dynabook": bobDevice.deviceEd25519Key, - "curve25519:Dynabook": bobDevice.deviceCurve25519Key, + "ed25519:Dynabook": bobDevice.deviceEd25519Key!, + "curve25519:Dynabook": bobDevice.deviceCurve25519Key!, }, + verified: DeviceInfo.DeviceVerification.UNVERIFIED, + known: false, }, }; @@ -136,7 +136,7 @@ describe("SAS verification", function() { bobPromise = new Promise>((resolve, reject) => { bob.client.on(CryptoEvent.VerificationRequest, request => { - request.verifier!.on("show_sas", (e) => { + (request.verifier!).on(SasEvent.ShowSas, (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); } else if (!aliceSasEvent) { @@ -158,7 +158,7 @@ describe("SAS verification", function() { aliceVerifier = alice.client.beginKeyVerification( verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!, - ); + ) as SAS; aliceVerifier.on(SasEvent.ShowSas, (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); @@ -413,7 +413,7 @@ describe("SAS verification", function() { const bobPromise = new Promise>((resolve, reject) => { bob.client.on(CryptoEvent.VerificationRequest, request => { - request.verifier!.on("show_sas", (e) => { + (request.verifier!).on(SasEvent.ShowSas, (e) => { e.mismatch(); }); resolve(request.verifier!); @@ -443,13 +443,13 @@ describe("SAS verification", function() { }); describe("verification in DM", function() { - let alice; - let bob; - let aliceSasEvent; - let bobSasEvent; - let aliceVerifier; - let bobPromise; - let clearTestClientTimeouts; + let alice: TestClient; + let bob: TestClient; + let aliceSasEvent: ISasEvent | null; + let bobSasEvent: ISasEvent | null; + let aliceVerifier: SAS; + let bobPromise: Promise; + let clearTestClientTimeouts: Function; beforeEach(async function() { [[alice, bob], clearTestClientTimeouts] = await makeTestClients( @@ -477,7 +477,7 @@ describe("SAS verification", function() { ); }; alice.client.downloadKeys = () => { - return Promise.resolve(); + return Promise.resolve({}); }; bob.client.crypto!.setDeviceVerification = jest.fn(); @@ -495,16 +495,16 @@ describe("SAS verification", function() { return "bob+base64+ed25519+key"; }; bob.client.downloadKeys = () => { - return Promise.resolve(); + return Promise.resolve({}); }; aliceSasEvent = null; bobSasEvent = null; bobPromise = new Promise((resolve, reject) => { - bob.client.on("crypto.verification.request", async (request) => { - const verifier = request.beginKeyVerification(SAS.NAME); - verifier.on("show_sas", (e) => { + bob.client.on(CryptoEvent.VerificationRequest, async (request) => { + const verifier = request.beginKeyVerification(SAS.NAME) as SAS; + verifier.on(SasEvent.ShowSas, (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); } else if (!aliceSasEvent) { @@ -525,12 +525,10 @@ describe("SAS verification", function() { }); }); - const aliceRequest = await alice.client.requestVerificationDM( - bob.client.getUserId(), "!room_id", - ); + const aliceRequest = await alice.client.requestVerificationDM(bob.client.getUserId()!, "!room_id"); await aliceRequest.waitFor(r => r.started); - aliceVerifier = aliceRequest.verifier; - aliceVerifier.on("show_sas", (e) => { + aliceVerifier = aliceRequest.verifier! as SAS; + aliceVerifier.on(SasEvent.ShowSas, (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); } else if (!bobSasEvent) { diff --git a/spec/unit/crypto/verification/secret_request.spec.ts b/spec/unit/crypto/verification/secret_request.spec.ts index 2f9fafb5dfc..20ea64d4ac3 100644 --- a/spec/unit/crypto/verification/secret_request.spec.ts +++ b/spec/unit/crypto/verification/secret_request.spec.ts @@ -125,7 +125,7 @@ describe("self-verifications", () => { expect(restoreKeyBackupWithCache).toHaveBeenCalled(); expect(result).toBeInstanceOf(Array); - expect(result[0][0]).toBe(testKeyPub); - expect(result[1][0]).toBe(testKeyPub); + expect(result![0][0]).toBe(testKeyPub); + expect(result![1][0]).toBe(testKeyPub); }); }); diff --git a/spec/unit/crypto/verification/util.ts b/spec/unit/crypto/verification/util.ts index 5efbe4ed5bd..f66d6464f5e 100644 --- a/spec/unit/crypto/verification/util.ts +++ b/spec/unit/crypto/verification/util.ts @@ -16,13 +16,21 @@ limitations under the License. */ import { TestClient } from '../../../TestClient'; -import { MatrixEvent } from "../../../../src/models/event"; +import { IContent, MatrixEvent } from "../../../../src/models/event"; import { IRoomTimelineData } from "../../../../src/models/event-timeline-set"; import { Room, RoomEvent } from "../../../../src/models/room"; import { logger } from '../../../../src/logger'; -import { MatrixClient, ClientEvent } from '../../../../src/client'; +import { MatrixClient, ClientEvent, ICreateClientOpts } from '../../../../src/client'; -export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> { +interface UserInfo { + userId: string; + deviceId: string; +} + +export async function makeTestClients( + userInfos: UserInfo[], + options: Partial, +): Promise<[TestClient[], () => void]> { const clients: TestClient[] = []; const timeouts: ReturnType[] = []; const clientMap: Record> = {}; @@ -51,7 +59,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[] } return {}; }; - const makeSendEvent = (matrixClient: MatrixClient) => (room, type, content) => { + const makeSendEvent = (matrixClient: MatrixClient) => (room: string, type: string, content: IContent) => { // make up a unique ID as the event ID const eventId = "$" + matrixClient.makeTxnId(); const rawEvent = { @@ -88,11 +96,12 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[] }; for (const userInfo of userInfos) { - let keys = {}; + let keys: Record = {}; if (!options) options = {}; if (!options.cryptoCallbacks) options.cryptoCallbacks = {}; if (!options.cryptoCallbacks.saveCrossSigningKeys) { options.cryptoCallbacks.saveCrossSigningKeys = k => { keys = k; }; + // @ts-ignore tsc getting confused by overloads options.cryptoCallbacks.getCrossSigningKey = typ => keys[typ]; } const testClient = new TestClient( @@ -104,6 +113,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[] } clientMap[userInfo.userId][userInfo.deviceId] = testClient.client; testClient.client.sendToDevice = makeSendToDevice(testClient.client); + // @ts-ignore tsc getting confused by overloads testClient.client.sendEvent = makeSendEvent(testClient.client); clients.push(testClient); } diff --git a/spec/unit/crypto/verification/verification_request.spec.ts b/spec/unit/crypto/verification/verification_request.spec.ts index 6549c3af78a..2dce928e821 100644 --- a/spec/unit/crypto/verification/verification_request.spec.ts +++ b/spec/unit/crypto/verification/verification_request.spec.ts @@ -18,7 +18,7 @@ import { VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE } from import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel"; import { ToDeviceChannel } from "../../../../src/crypto/verification/request/ToDeviceChannel"; -import { MatrixEvent } from "../../../../src/models/event"; +import { IContent, MatrixEvent } from "../../../../src/models/event"; import { MatrixClient } from "../../../../src/client"; import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel"; import { VerificationBase } from "../../../../src/crypto/verification/Base"; @@ -30,12 +30,12 @@ type MockClient = MatrixClient & { function makeMockClient(userId: string, deviceId: string): MockClient { let counter = 1; let events: MatrixEvent[] = []; - const deviceEvents = {}; + const deviceEvents: Record> = {}; return { getUserId() { return userId; }, getDeviceId() { return deviceId; }, - sendEvent(roomId, type, content) { + sendEvent(roomId: string, type: string, content: IContent) { counter = counter + 1; const eventId = `$${userId}-${deviceId}-${counter}`; events.push(new MatrixEvent({ @@ -49,7 +49,7 @@ function makeMockClient(userId: string, deviceId: string): MockClient { return Promise.resolve({ event_id: eventId }); }, - sendToDevice(type, msgMap) { + sendToDevice(type: string, msgMap: Record>) { for (const userId of Object.keys(msgMap)) { const deviceMap = msgMap[userId]; for (const deviceId of Object.keys(deviceMap)) { @@ -111,7 +111,7 @@ class MockVerifier extends VerificationBase<'', any> { } } - async handleEvent(event) { + async handleEvent(event: MatrixEvent) { if (event.getType() === DONE_TYPE && !this._startEvent) { await this._channel.send(DONE_TYPE, {}); } @@ -122,7 +122,7 @@ class MockVerifier extends VerificationBase<'', any> { } } -function makeRemoteEcho(event) { +function makeRemoteEcho(event: MatrixEvent) { return new MatrixEvent(Object.assign({}, event.event, { unsigned: { transaction_id: "abc", diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index ba4be9f9b20..419ef50206c 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -17,7 +17,7 @@ limitations under the License. import { mocked } from "jest-mock"; import { logger } from "../../src/logger"; -import { MatrixClient, ClientEvent } from "../../src/client"; +import { ClientEvent, ITurnServerResponse, MatrixClient, Store } from "../../src/client"; import { Filter } from "../../src/filter"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace"; import { @@ -36,7 +36,16 @@ import { ReceiptType } from "../../src/@types/read_receipts"; import * as testUtils from "../test-utils/test-utils"; import { makeBeaconInfoContent } from "../../src/content-helpers"; import { M_BEACON_INFO } from "../../src/@types/beacon"; -import { ContentHelpers, EventTimeline, MatrixError, Room } from "../../src"; +import { + ContentHelpers, + EventTimeline, ICreateRoomOpts, + IRequestOpts, + MatrixError, + MatrixHttpApi, + MatrixScheduler, + Method, + Room, +} from "../../src"; import { supportsMatrixCall } from "../../src/webrtc/call"; import { makeBeaconEvent } from "../test-utils/beacon"; import { @@ -44,6 +53,9 @@ import { POLICIES_ACCOUNT_EVENT_TYPE, PolicyScope, } from "../../src/models/invites-ignorer"; +import { IOlmDevice } from "../../src/crypto/algorithms/megolm"; +import { QueryDict } from "../../src/utils"; +import { SyncState } from "../../src/sync"; jest.useFakeTimers(); @@ -52,17 +64,36 @@ jest.mock("../../src/webrtc/call", () => ({ supportsMatrixCall: jest.fn(() => false), })); +type HttpLookup = { + method: string; + path: string; + data?: Record; + error?: object; + expectBody?: Record; + expectQueryParams?: QueryDict; + thenCall?: Function; +}; + +interface Options extends ICreateRoomOpts { + _roomId?: string; +} + +type WrappedRoom = Room & { + _options: Options; + _state: Map; +}; + describe("MatrixClient", function() { const userId = "@alice:bar"; const identityServerUrl = "https://identity.server"; const identityServerDomain = "identity.server"; - let client; - let store; - let scheduler; + let client: MatrixClient; + let store: Store; + let scheduler: MatrixScheduler; const KEEP_ALIVE_PATH = "/_matrix/client/versions"; - const PUSH_RULES_RESPONSE = { + const PUSH_RULES_RESPONSE: HttpLookup = { method: "GET", path: "/pushrules/", data: {}, @@ -70,7 +101,7 @@ describe("MatrixClient", function() { const FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter"; - const FILTER_RESPONSE = { + const FILTER_RESPONSE: HttpLookup = { method: "POST", path: FILTER_PATH, data: { filter_id: "f1lt3r" }, @@ -82,29 +113,21 @@ describe("MatrixClient", function() { rooms: {}, }; - const SYNC_RESPONSE = { + const SYNC_RESPONSE: HttpLookup = { method: "GET", path: "/sync", data: SYNC_DATA, }; // items are popped off when processed and block if no items left. - let httpLookups: { - method: string; - path: string; - data?: object; - error?: object; - expectBody?: object; - expectQueryParams?: object; - thenCall?: Function; - }[] = []; + let httpLookups: HttpLookup[] = []; let acceptKeepalives: boolean; let pendingLookup: { promise: Promise; method: string; path: string; } | null = null; - function httpReq(method, path, qp, data, prefix) { + function httpReq(method: Method, path: string, qp?: QueryDict, data?: BodyInit, opts?: IRequestOpts) { if (path === KEEP_ALIVE_PATH && acceptKeepalives) { return Promise.resolve({ unstable_features: { @@ -145,7 +168,7 @@ describe("MatrixClient", function() { } if (next.expectQueryParams) { Object.keys(next.expectQueryParams).forEach(function(k) { - expect(qp[k]).toEqual(next.expectQueryParams![k]); + expect(qp?.[k]).toEqual(next.expectQueryParams![k]); }); } @@ -184,24 +207,38 @@ describe("MatrixClient", function() { userId: userId, }); // FIXME: We shouldn't be yanking http like this. - client.http = [ - "authedRequest", "getContentUri", "request", "uploadContent", - ].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); - client.http.authedRequest.mockImplementation(httpReq); - client.http.request.mockImplementation(httpReq); + client.http = ([ + "authedRequest", + "getContentUri", + "request", + "uploadContent", + ] as const).reduce((r, k) => { + r[k] = jest.fn(); + return r; + }, {} as MatrixHttpApi); + mocked(client.http.authedRequest).mockImplementation(httpReq); + mocked(client.http.request).mockImplementation(httpReq); } beforeEach(function() { - scheduler = [ - "getQueueForEvent", "queueEvent", "removeEventFromQueue", + scheduler = ([ + "getQueueForEvent", + "queueEvent", + "removeEventFromQueue", "setProcessFunction", - ].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); - store = [ + ] as const).reduce((r, k) => { + r[k] = jest.fn(); + return r; + }, {} as MatrixScheduler); + store = ([ "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter", - "getSyncAccumulator", "startup", "deleteAllData", - ].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); + "startup", "deleteAllData", + ] as const).reduce((r, k) => { + r[k] = jest.fn(); + return r; + }, {} as Store); store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null)); store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null)); store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null)); @@ -225,7 +262,7 @@ describe("MatrixClient", function() { // means they may call /events and then fail an expect() which will fail // a DIFFERENT test (pollution between tests!) - we return unresolved // promises to stop the client from continuing to run. - client.http.authedRequest.mockImplementation(function() { + mocked(client.http.authedRequest).mockImplementation(function() { return new Promise(() => {}); }); client.stopClient(); @@ -289,7 +326,7 @@ describe("MatrixClient", function() { const txnId = client.makeTxnId(); const room = new Room(roomId, client, userId); - store.getRoom.mockReturnValue(room); + mocked(store.getRoom).mockReturnValue(room); const rootEvent = new MatrixEvent({ event_id: threadId }); room.createThread(threadId, rootEvent, [rootEvent], false); @@ -329,7 +366,7 @@ describe("MatrixClient", function() { }; const room = new Room(roomId, client, userId); - store.getRoom.mockReturnValue(room); + mocked(store.getRoom).mockReturnValue(room); const rootEvent = new MatrixEvent({ event_id: threadId }); room.createThread(threadId, rootEvent, [rootEvent], false); @@ -359,7 +396,7 @@ describe("MatrixClient", function() { const userId = "@test:example.org"; const roomId = "!room:example.org"; const roomName = "Test Tree"; - const mockRoom = {}; + const mockRoom = {} as unknown as Room; const fn = jest.fn().mockImplementation((opts) => { expect(opts).toMatchObject({ name: roomName, @@ -431,23 +468,23 @@ describe("MatrixClient", function() { throw new Error("Unexpected event type or state key"); } }, - }, - }; + } as Room["currentState"], + } as unknown as Room; client.getRoom = (getRoomId) => { expect(getRoomId).toEqual(roomId); return mockRoom; }; const tree = client.unstableGetFileTreeSpace(roomId); expect(tree).toBeDefined(); - expect(tree.roomId).toEqual(roomId); - expect(tree.room).toBe(mockRoom); + expect(tree!.roomId).toEqual(roomId); + expect(tree!.room).toBe(mockRoom); }); it("should not get (unstable) file trees if not joined", async () => { const roomId = "!room:example.org"; const mockRoom = { getMyMembership: () => "leave", // "not join" - }; + } as unknown as Room; client.getRoom = (getRoomId) => { expect(getRoomId).toEqual(roomId); return mockRoom; @@ -491,8 +528,8 @@ describe("MatrixClient", function() { throw new Error("Unexpected event type or state key"); } }, - }, - }; + } as Room["currentState"], + } as unknown as Room; client.getRoom = (getRoomId) => { expect(getRoomId).toEqual(roomId); return mockRoom; @@ -525,8 +562,8 @@ describe("MatrixClient", function() { throw new Error("Unexpected event type or state key"); } }, - }, - }; + } as Room["currentState"], + } as unknown as Room; client.getRoom = (getRoomId) => { expect(getRoomId).toEqual(roomId); return mockRoom; @@ -541,15 +578,15 @@ describe("MatrixClient", function() { SYNC_RESPONSE, ]; const filterId = "ehfewf"; - store.getFilterIdByName.mockReturnValue(filterId); + mocked(store.getFilterIdByName).mockReturnValue(filterId); const filter = new Filter("0", filterId); filter.setDefinition({ "room": { "timeline": { "limit": 8 } } }); - store.getFilter.mockReturnValue(filter); + mocked(store.getFilter).mockReturnValue(filter); const syncPromise = new Promise((resolve, reject) => { - client.on("sync", function syncListener(state) { + client.on(ClientEvent.Sync, function syncListener(state) { if (state === "SYNCING") { expect(httpLookups.length).toEqual(0); - client.removeListener("sync", syncListener); + client.removeListener(ClientEvent.Sync, syncListener); resolve(); } else if (state === "ERROR") { reject(new Error("sync error")); @@ -567,10 +604,10 @@ describe("MatrixClient", function() { it("should return the same sync state as emitted sync events", async function() { const syncingPromise = new Promise((resolve) => { - client.on("sync", function syncListener(state) { + client.on(ClientEvent.Sync, function syncListener(state) { expect(state).toEqual(client.getSyncState()); if (state === "SYNCING") { - client.removeListener("sync", syncListener); + client.removeListener(ClientEvent.Sync, syncListener); resolve(); } }); @@ -586,7 +623,7 @@ describe("MatrixClient", function() { it("should use an existing filter if id is present in localStorage", function() { }); it("should handle localStorage filterId missing from the server", function(done) { - function getFilterName(userId, suffix?: string) { + function getFilterName(userId: string, suffix?: string) { // scope this on the user ID because people may login on many accounts // and they all need to be stored! return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : ""); @@ -605,14 +642,14 @@ describe("MatrixClient", function() { }, }); httpLookups.push(FILTER_RESPONSE); - store.getFilterIdByName.mockReturnValue(invalidFilterId); + mocked(store.getFilterIdByName).mockReturnValue(invalidFilterId); - const filterName = getFilterName(client.credentials.userId); + const filterName = getFilterName(client.credentials.userId!); client.store.setFilterIdByName(filterName, invalidFilterId); const filter = new Filter(client.credentials.userId); client.getOrCreateFilter(filterName, filter).then(function(filterId) { - expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id); + expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id); done(); }); }); @@ -634,13 +671,13 @@ describe("MatrixClient", function() { httpLookups.push(FILTER_RESPONSE); httpLookups.push(SYNC_RESPONSE); - client.on("sync", function syncListener(state) { + client.on(ClientEvent.Sync, function syncListener(state) { if (state === "ERROR" && httpLookups.length > 0) { expect(httpLookups.length).toEqual(2); expect(client.retryImmediately()).toBe(true); jest.advanceTimersByTime(1); } else if (state === "PREPARED" && httpLookups.length === 0) { - client.removeListener("sync", syncListener); + client.removeListener(ClientEvent.Sync, syncListener); done(); } else { // unexpected state transition! @@ -658,7 +695,7 @@ describe("MatrixClient", function() { method: "GET", path: "/sync", data: SYNC_DATA, }); - client.on("sync", function syncListener(state) { + client.on(ClientEvent.Sync, function syncListener(state) { if (state === "ERROR" && httpLookups.length > 0) { expect(httpLookups.length).toEqual(1); expect(client.retryImmediately()).toBe( @@ -668,7 +705,7 @@ describe("MatrixClient", function() { } else if (state === "RECONNECTING" && httpLookups.length > 0) { jest.advanceTimersByTime(10000); } else if (state === "SYNCING" && httpLookups.length === 0) { - client.removeListener("sync", syncListener); + client.removeListener(ClientEvent.Sync, syncListener); done(); } }); @@ -684,13 +721,13 @@ describe("MatrixClient", function() { httpLookups.push(FILTER_RESPONSE); httpLookups.push(SYNC_RESPONSE); - client.on("sync", function syncListener(state) { + client.on(ClientEvent.Sync, function syncListener(state) { if (state === "ERROR" && httpLookups.length > 0) { expect(httpLookups.length).toEqual(3); expect(client.retryImmediately()).toBe(true); jest.advanceTimersByTime(1); } else if (state === "PREPARED" && httpLookups.length === 0) { - client.removeListener("sync", syncListener); + client.removeListener(ClientEvent.Sync, syncListener); done(); } else { // unexpected state transition! @@ -702,8 +739,8 @@ describe("MatrixClient", function() { }); describe("emitted sync events", function() { - function syncChecker(expectedStates, done) { - return function syncListener(state, old) { + function syncChecker(expectedStates: [string, string | null][], done: Function) { + return function syncListener(state: SyncState, old: SyncState | null) { const expected = expectedStates.shift(); logger.log( "'sync' curr=%s old=%s EXPECT=%s", state, old, expected, @@ -715,7 +752,7 @@ describe("MatrixClient", function() { expect(state).toEqual(expected[0]); expect(old).toEqual(expected[1]); if (expectedStates.length === 0) { - client.removeListener("sync", syncListener); + client.removeListener(ClientEvent.Sync, syncListener); done(); } // standard retry time is 5 to 10 seconds @@ -726,7 +763,7 @@ describe("MatrixClient", function() { it("should transition null -> PREPARED after the first /sync", function(done) { const expectedStates: [string, string | null][] = []; expectedStates.push(["PREPARED", null]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -738,7 +775,7 @@ describe("MatrixClient", function() { method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }, }); expectedStates.push(["ERROR", null]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -768,7 +805,7 @@ describe("MatrixClient", function() { expectedStates.push(["RECONNECTING", null]); expectedStates.push(["ERROR", "RECONNECTING"]); expectedStates.push(["CATCHUP", "ERROR"]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -776,7 +813,7 @@ describe("MatrixClient", function() { const expectedStates: [string, string | null][] = []; expectedStates.push(["PREPARED", null]); expectedStates.push(["SYNCING", "PREPARED"]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -795,7 +832,7 @@ describe("MatrixClient", function() { expectedStates.push(["SYNCING", "PREPARED"]); expectedStates.push(["RECONNECTING", "SYNCING"]); expectedStates.push(["ERROR", "RECONNECTING"]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -809,7 +846,7 @@ describe("MatrixClient", function() { expectedStates.push(["PREPARED", null]); expectedStates.push(["SYNCING", "PREPARED"]); expectedStates.push(["ERROR", "SYNCING"]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -821,7 +858,7 @@ describe("MatrixClient", function() { expectedStates.push(["PREPARED", null]); expectedStates.push(["SYNCING", "PREPARED"]); expectedStates.push(["SYNCING", "SYNCING"]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -845,7 +882,7 @@ describe("MatrixClient", function() { expectedStates.push(["RECONNECTING", "SYNCING"]); expectedStates.push(["ERROR", "RECONNECTING"]); expectedStates.push(["ERROR", "ERROR"]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); }); @@ -914,14 +951,14 @@ describe("MatrixClient", function() { throw new Error("Unexpected event type or state key"); } }, - }, + } as Room["currentState"], getThread: jest.fn(), addPendingEvent: jest.fn(), updatePendingEvent: jest.fn(), reEmitter: { reEmit: jest.fn(), }, - }; + } as unknown as Room; beforeEach(() => { client.getRoom = (getRoomId) => { @@ -987,7 +1024,7 @@ describe("MatrixClient", function() { const mockRoom = { getMyMembership: () => "join", - updatePendingEvent: (event, status) => event.setStatus(status), + updatePendingEvent: (event: MatrixEvent, status: EventStatus) => event.setStatus(status), currentState: { getStateEvents: (eventType, stateKey) => { if (eventType === EventType.RoomCreate) { @@ -1004,15 +1041,14 @@ describe("MatrixClient", function() { throw new Error("Unexpected event type or state key"); } }, - }, - }; + } as Room["currentState"], + } as unknown as Room; - let event; + let event: MatrixEvent; beforeEach(async () => { event = new MatrixEvent({ event_id: "~" + roomId + ":" + txnId, - user_id: client.credentials.userId, - sender: client.credentials.userId, + sender: client.credentials.userId!, room_id: roomId, origin_server_ts: new Date().getTime(), }); @@ -1023,25 +1059,26 @@ describe("MatrixClient", function() { return mockRoom; }; client.crypto = { // mock crypto - encryptEvent: (event, room) => new Promise(() => {}), + encryptEvent: () => new Promise(() => {}), stop: jest.fn(), - }; + } as unknown as Crypto; }); function assertCancelled() { expect(event.status).toBe(EventStatus.CANCELLED); - expect(client.scheduler.removeEventFromQueue(event)).toBeFalsy(); + expect(client.scheduler?.removeEventFromQueue(event)).toBeFalsy(); expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0); } it("should cancel an event which is queued", () => { event.setStatus(EventStatus.QUEUED); - client.scheduler.queueEvent(event); + client.scheduler?.queueEvent(event); client.cancelPendingEvent(event); assertCancelled(); }); it("should cancel an event which is encrypting", async () => { + // @ts-ignore protected method access client.encryptAndSendEvent(null, event); await testUtils.emitPromise(event, "Event.status"); client.cancelPendingEvent(event); @@ -1103,7 +1140,7 @@ describe("MatrixClient", function() { const room = { hasPendingEvent: jest.fn().mockReturnValue(false), addLocalEchoReceipt: jest.fn(), - }; + } as unknown as Room; const rrEvent = new MatrixEvent({ event_id: "read_event_id" }); const rpEvent = new MatrixEvent({ event_id: "read_private_event_id" }); client.getRoom = () => room; @@ -1142,7 +1179,7 @@ describe("MatrixClient", function() { const content = makeBeaconInfoContent(100, true); beforeEach(() => { - client.http.authedRequest.mockClear().mockResolvedValue({}); + mocked(client.http.authedRequest).mockClear().mockResolvedValue({}); }); it("creates new beacon info", async () => { @@ -1150,7 +1187,7 @@ describe("MatrixClient", function() { // event type combined const expectedEventType = M_BEACON_INFO.name; - const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0]; + const [method, path, queryParams, requestContent] = mocked(client.http.authedRequest).mock.calls[0]; expect(method).toBe('PUT'); expect(path).toEqual( `/rooms/${encodeURIComponent(roomId)}/state/` + @@ -1164,7 +1201,7 @@ describe("MatrixClient", function() { await client.unstable_setLiveBeacon(roomId, content); // event type combined - const [, path, , requestContent] = client.http.authedRequest.mock.calls[0]; + const [, path, , requestContent] = mocked(client.http.authedRequest).mock.calls[0]; expect(path).toEqual( `/rooms/${encodeURIComponent(roomId)}/state/` + `${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`, @@ -1242,7 +1279,7 @@ describe("MatrixClient", function() { const newPassword = 'newpassword'; const passwordTest = (expectedRequestContent: any) => { - const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0]; + const [method, path, queryParams, requestContent] = mocked(client.http.authedRequest).mock.calls[0]; expect(method).toBe('POST'); expect(path).toEqual('/account/password'); expect(queryParams).toBeFalsy(); @@ -1250,7 +1287,7 @@ describe("MatrixClient", function() { }; beforeEach(() => { - client.http.authedRequest.mockClear().mockResolvedValue({}); + mocked(client.http.authedRequest).mockClear().mockResolvedValue({}); }); it("no logout_devices specified", async () => { @@ -1289,13 +1326,13 @@ describe("MatrixClient", function() { const response = { aliases: ["#woop:example.org", "#another:example.org"], }; - client.http.authedRequest.mockClear().mockResolvedValue(response); + mocked(client.http.authedRequest).mockClear().mockResolvedValue(response); const roomId = "!whatever:example.org"; const result = await client.getLocalAliases(roomId); // Current version of the endpoint we support is v3 - const [method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0]; + const [method, path, queryParams, data, opts] = mocked(client.http.authedRequest).mock.calls[0]; expect(data).toBeFalsy(); expect(method).toBe('GET'); expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`); @@ -1352,11 +1389,11 @@ describe("MatrixClient", function() { ], username: "1443779631:@user:example.com", password: "JlKfBy1QwLrO20385QyAtEyIv0=", - }; + } as unknown as ITurnServerResponse; jest.spyOn(client, "turnServer").mockResolvedValue(turnServer); const events: any[][] = []; - const onTurnServers = (...args) => events.push(args); + const onTurnServers = (...args: any[]) => events.push(args); client.on(ClientEvent.TurnServers, onTurnServers); expect(await client.checkTurnServers()).toBe(true); client.off(ClientEvent.TurnServers, onTurnServers); @@ -1372,7 +1409,7 @@ describe("MatrixClient", function() { jest.spyOn(client, "turnServer").mockRejectedValue(error); const events: any[][] = []; - const onTurnServersError = (...args) => events.push(args); + const onTurnServersError = (...args: any[]) => events.push(args); client.on(ClientEvent.TurnServersError, onTurnServersError); expect(await client.checkTurnServers()).toBe(false); client.off(ClientEvent.TurnServersError, onTurnServersError); @@ -1384,7 +1421,7 @@ describe("MatrixClient", function() { jest.spyOn(client, "turnServer").mockRejectedValue(error); const events: any[][] = []; - const onTurnServersError = (...args) => events.push(args); + const onTurnServersError = (...args: any[]) => events.push(args); client.on(ClientEvent.TurnServersError, onTurnServersError); expect(await client.checkTurnServers()).toBe(false); client.off(ClientEvent.TurnServersError, onTurnServersError); @@ -1400,7 +1437,7 @@ describe("MatrixClient", function() { it("is an alias for the crypto method", async () => { client.crypto = testUtils.mock(Crypto, "Crypto"); - const deviceInfos = []; + const deviceInfos: IOlmDevice[] = []; const payload = {}; await client.encryptAndSendToDevices(deviceInfos, payload); expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload); @@ -1413,7 +1450,7 @@ describe("MatrixClient", function() { const dataStore = new Map(); client.setAccountData = function(eventType, content) { dataStore.set(eventType, content); - return Promise.resolve(); + return Promise.resolve({}); }; client.getAccountData = function(eventType) { const data = dataStore.get(eventType); @@ -1424,9 +1461,9 @@ describe("MatrixClient", function() { // Mockup `createRoom`/`getRoom`/`joinRoom`, including state. const rooms = new Map(); - client.createRoom = function(options = {}) { + client.createRoom = function(options: Options = {}) { const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`; - const state = new Map(); + const state = new Map(); const room = { roomId, _options: options, @@ -1444,24 +1481,24 @@ describe("MatrixClient", function() { }, }; }, - }; + } as EventTimeline; }, }; }, - }; + } as unknown as WrappedRoom; rooms.set(roomId, room); return Promise.resolve({ room_id: roomId }); }; client.getRoom = function(roomId) { return rooms.get(roomId); }; - client.joinRoom = function(roomId) { - return this.getRoom(roomId) || this.createRoom({ _roomId: roomId }); + client.joinRoom = async function(roomId) { + return this.getRoom(roomId)! || this.createRoom({ _roomId: roomId } as ICreateRoomOpts); }; // Mockup state events client.sendStateEvent = function(roomId, type, content) { - const room = this.getRoom(roomId); + const room = this.getRoom(roomId) as WrappedRoom; const state: Map = room._state; let store = state.get(type); if (!store) { @@ -1480,14 +1517,15 @@ describe("MatrixClient", function() { return content; }, }; - return { event_id: eventId }; + return Promise.resolve({ event_id: eventId }); }; client.redactEvent = function(roomId, eventId) { - const room = this.getRoom(roomId); + const room = this.getRoom(roomId) as WrappedRoom; const state: Map = room._state; for (const store of state.values()) { - delete store[eventId]; + delete store[eventId!]; } + return Promise.resolve({ event_id: "$" + eventId + "-" + Math.random() }); }; }); @@ -1523,7 +1561,7 @@ describe("MatrixClient", function() { roomId: "!snafu:somewhere.org", }); expect(ruleMatch).toBeTruthy(); - expect(ruleMatch.getContent()).toMatchObject({ + expect(ruleMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: "just a test", }); @@ -1552,7 +1590,7 @@ describe("MatrixClient", function() { roomId: "!snafu:somewhere.org", }); expect(ruleSenderMatch).toBeTruthy(); - expect(ruleSenderMatch.getContent()).toMatchObject({ + expect(ruleSenderMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: REASON, }); @@ -1562,7 +1600,7 @@ describe("MatrixClient", function() { roomId: "!snafu:example.org", }); expect(ruleRoomMatch).toBeTruthy(); - expect(ruleRoomMatch.getContent()).toMatchObject({ + expect(ruleRoomMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: REASON, }); @@ -1587,7 +1625,7 @@ describe("MatrixClient", function() { roomId: BAD_ROOM_ID, }); expect(ruleSenderMatch).toBeTruthy(); - expect(ruleSenderMatch.getContent()).toMatchObject({ + expect(ruleSenderMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: REASON, }); @@ -1621,7 +1659,7 @@ describe("MatrixClient", function() { roomId: "!snafu:somewhere.org", }); expect(ruleMatch).toBeTruthy(); - expect(ruleMatch.getContent()).toMatchObject({ + expect(ruleMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: "just a test", }); @@ -1649,13 +1687,13 @@ describe("MatrixClient", function() { roomId: "!snafu:somewhere.org", }); expect(ruleMatch).toBeTruthy(); - expect(ruleMatch.getContent()).toMatchObject({ + expect(ruleMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: "just a test", }); // After removing the invite, we shouldn't reject it anymore. - await client.ignoredInvites.removeRule(ruleMatch); + await client.ignoredInvites.removeRule(ruleMatch as MatrixEvent); const ruleMatch2 = await client.ignoredInvites.getRuleForInvite({ sender: "@foobar:example.org", roomId: "!snafu:somewhere.org", @@ -1669,10 +1707,10 @@ describe("MatrixClient", function() { // Make sure that everything is initialized. await client.ignoredInvites.getOrCreateSourceRooms(); await client.joinRoom(NEW_SOURCE_ROOM_ID); - const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID); + const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID) as WrappedRoom; // Fetch the list of sources and check that we do not have the new room yet. - const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent(); + const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name)!.getContent(); expect(policies).toBeTruthy(); const ignoreInvites = policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name]; expect(ignoreInvites).toBeTruthy(); @@ -1686,7 +1724,7 @@ describe("MatrixClient", function() { expect(added2).toBe(false); // Fetch the list of sources and check that we have added the new room. - const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent(); + const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name)!.getContent(); expect(policies2).toBeTruthy(); const ignoreInvites2 = policies2[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name]; expect(ignoreInvites2).toBeTruthy(); @@ -1698,7 +1736,7 @@ describe("MatrixClient", function() { // Check where it shows up. const targetRoomId = ignoreInvites2.target; - const targetRoom = client.getRoom(targetRoomId); + const targetRoom = client.getRoom(targetRoomId) as WrappedRoom; expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy(); expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy(); }); diff --git a/spec/unit/models/MSC3089TreeSpace.spec.ts b/spec/unit/models/MSC3089TreeSpace.spec.ts index 4158ea8b9fe..ab39b907535 100644 --- a/spec/unit/models/MSC3089TreeSpace.spec.ts +++ b/spec/unit/models/MSC3089TreeSpace.spec.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from "../../../src"; +import { IContent, MatrixClient } from "../../../src"; import { Room } from "../../../src/models/room"; import { MatrixEvent } from "../../../src/models/event"; import { EventType, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../../../src/@types/event"; @@ -33,7 +33,7 @@ describe("MSC3089TreeSpace", () => { const roomId = "!tree:localhost"; const targetUser = "@target:example.org"; - let powerLevels; + let powerLevels: MatrixEvent; beforeEach(() => { // TODO: Use utility functions to create test rooms and clients @@ -480,7 +480,7 @@ describe("MSC3089TreeSpace", () => { const staticDomain = "static.example.org"; function addSubspace(roomId: string, createTs?: number, order?: string) { - const content = { + const content: IContent = { via: [staticDomain], }; if (order) content['order'] = order; diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index 535e0f12db6..92d929fd4e7 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -121,7 +121,7 @@ describe('MatrixEvent', () => { }); describe(".attemptDecryption", () => { - let encryptedEvent; + let encryptedEvent: MatrixEvent; const eventId = 'test_encrypted_event'; beforeEach(() => { @@ -155,7 +155,7 @@ describe('MatrixEvent', () => { }, }); }), - }; + } as unknown as Crypto; await encryptedEvent.attemptDecryption(crypto); diff --git a/spec/unit/notifications.spec.ts b/spec/unit/notifications.spec.ts index e7e637d4acc..02388f9f0dd 100644 --- a/spec/unit/notifications.spec.ts +++ b/spec/unit/notifications.spec.ts @@ -39,7 +39,7 @@ let threadEvent: MatrixEvent; const ROOM_ID = "!roomId:example.org"; let THREAD_ID: string; -function mkPushAction(notify, highlight): IActionsObject { +function mkPushAction(notify: boolean, highlight: boolean): IActionsObject { return { notify, tweaks: { diff --git a/spec/unit/read-receipt.spec.ts b/spec/unit/read-receipt.spec.ts index 4443c25befc..df902d13b1c 100644 --- a/spec/unit/read-receipt.spec.ts +++ b/spec/unit/read-receipt.spec.ts @@ -70,7 +70,7 @@ const roomEvent = utils.mkEvent({ }, }); -function mockServerSideSupport(client, serverSideSupport: ServerSupport) { +function mockServerSideSupport(client: MatrixClient, serverSideSupport: ServerSupport) { client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport); } diff --git a/spec/unit/realtime-callbacks.spec.ts b/spec/unit/realtime-callbacks.spec.ts index dd0d605a0cc..dfbbfc9f77a 100644 --- a/spec/unit/realtime-callbacks.spec.ts +++ b/spec/unit/realtime-callbacks.spec.ts @@ -20,7 +20,7 @@ let wallTime = 1234567890; jest.useFakeTimers().setSystemTime(wallTime); describe("realtime-callbacks", function() { - function tick(millis) { + function tick(millis: number): void { wallTime += millis; jest.advanceTimersByTime(millis); } diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 2e0f492c046..bca7385982b 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -87,7 +87,7 @@ describe("Rendezvous", function() { }); let httpBackend: MockHttpBackend; - let fetchFn: typeof global.fetchFn; + let fetchFn: typeof global.fetch; let transports: DummyTransport[]; beforeEach(function() { diff --git a/spec/unit/room-member.spec.ts b/spec/unit/room-member.spec.ts index 8eb3096b60e..0ed96339bc2 100644 --- a/spec/unit/room-member.spec.ts +++ b/spec/unit/room-member.spec.ts @@ -373,37 +373,36 @@ describe("RoomMember", function() { expect(member.events.member).toEqual(joinEvent); }); - it("should set 'name' based on user_id, displayname and room state", - function() { - const roomState = { - getStateEvents: function(type) { - if (type !== "m.room.member") { - return []; - } - return [ - utils.mkMembership({ - event: true, mship: "join", room: roomId, - user: userB, - }), - utils.mkMembership({ - event: true, mship: "join", room: roomId, - user: userC, name: "Alice", - }), - joinEvent, - ]; - }, - getUserIdsWithDisplayName: function(displayName) { - return [userA, userC]; - }, - } as unknown as RoomState; - expect(member.name).toEqual(userA); // default = user_id - member.setMembershipEvent(joinEvent); - expect(member.name).toEqual("Alice"); // prefer displayname - member.setMembershipEvent(joinEvent, roomState); - expect(member.name).not.toEqual("Alice"); // it should disambig. - // user_id should be there somewhere - expect(member.name.indexOf(userA)).not.toEqual(-1); - }); + it("should set 'name' based on user_id, displayname and room state", function() { + const roomState = { + getStateEvents: function(type: string) { + if (type !== "m.room.member") { + return []; + } + return [ + utils.mkMembership({ + event: true, mship: "join", room: roomId, + user: userB, + }), + utils.mkMembership({ + event: true, mship: "join", room: roomId, + user: userC, name: "Alice", + }), + joinEvent, + ]; + }, + getUserIdsWithDisplayName: function(displayName: string) { + return [userA, userC]; + }, + } as unknown as RoomState; + expect(member.name).toEqual(userA); // default = user_id + member.setMembershipEvent(joinEvent); + expect(member.name).toEqual("Alice"); // prefer displayname + member.setMembershipEvent(joinEvent, roomState); + expect(member.name).not.toEqual("Alice"); // it should disambig. + // user_id should be there somewhere + expect(member.name.indexOf(userA)).not.toEqual(-1); + }); it("should emit 'RoomMember.membership' if the membership changes", function() { let emitCount = 0; @@ -455,7 +454,7 @@ describe("RoomMember", function() { }); const roomState = { - getStateEvents: function(type) { + getStateEvents: function(type: string) { if (type !== "m.room.member") { return []; } @@ -467,7 +466,7 @@ describe("RoomMember", function() { joinEvent, ]; }, - getUserIdsWithDisplayName: function(displayName) { + getUserIdsWithDisplayName: function(displayName: string) { return [userA, userC]; }, } as unknown as RoomState; diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index f6199570325..c820f37a074 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -19,27 +19,32 @@ limitations under the License. * @module client */ +import { mocked } from "jest-mock"; + import * as utils from "../test-utils/test-utils"; +import { emitPromise } from "../test-utils/test-utils"; import { + Direction, DuplicateStrategy, EventStatus, EventTimelineSet, - EventType, IStateEventWithRoomId, + EventType, IContent, + IStateEventWithRoomId, JoinRule, MatrixEvent, MatrixEventEvent, PendingEventOrdering, RelationType, RoomEvent, + RoomMember, } from "../../src"; import { EventTimeline } from "../../src/models/event-timeline"; import { NotificationCountType, Room } from "../../src/models/room"; import { RoomState } from "../../src/models/room-state"; import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { TestClient } from "../TestClient"; -import { emitPromise } from "../test-utils/test-utils"; import { ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts"; -import { FeatureSupport, Thread, ThreadEvent, THREAD_RELATION_TYPE } from "../../src/models/thread"; +import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread"; import { Crypto } from "../../src/crypto"; describe("Room", function() { @@ -48,7 +53,7 @@ describe("Room", function() { const userB = "@bertha:bar"; const userC = "@clarissa:bar"; const userD = "@dorothy:bar"; - let room; + let room: Room; const mkMessage = () => utils.mkMessage({ event: true, @@ -131,13 +136,16 @@ describe("Room", function() { beforeEach(function() { room = new Room(roomId, new TestClient(userA, "device").client, userA); // mock RoomStates + // @ts-ignore room.oldState = room.getLiveTimeline().startState = utils.mock(RoomState, "oldState"); + // @ts-ignore room.currentState = room.getLiveTimeline().endState = utils.mock(RoomState, "currentState"); }); describe('getCreator', () => { it("should return the creator from m.room.create", function() { - room.currentState.getStateEvents.mockImplementation(function(type, key) { + // @ts-ignore - mocked doesn't handle overloads sanely + mocked(room.currentState.getStateEvents).mockImplementation(function(type, key) { if (type === EventType.RoomCreate && key === "") { return utils.mkEvent({ event: true, @@ -160,7 +168,8 @@ describe("Room", function() { const hsUrl = "https://my.home.server"; it("should return the URL from m.room.avatar preferentially", function() { - room.currentState.getStateEvents.mockImplementation(function(type, key) { + // @ts-ignore - mocked doesn't handle overloads sanely + mocked(room.currentState.getStateEvents).mockImplementation(function(type, key) { if (type === EventType.RoomAvatar && key === "") { return utils.mkEvent({ event: true, @@ -174,10 +183,10 @@ describe("Room", function() { }); } }); - const url = room.getAvatarUrl(hsUrl); + const url = room.getAvatarUrl(hsUrl, 100, 100, "scale"); // we don't care about how the mxc->http conversion is done, other // than it contains the mxc body. - expect(url.indexOf("flibble/wibble")).not.toEqual(-1); + expect(url?.indexOf("flibble/wibble")).not.toEqual(-1); }); it("should return nothing if there is no m.room.avatar and allowDefault=false", @@ -189,12 +198,12 @@ describe("Room", function() { describe("getMember", function() { beforeEach(function() { - room.currentState.getMember.mockImplementation(function(userId) { + mocked(room.currentState.getMember).mockImplementation(function(userId) { return { "@alice:bar": { userId: userA, roomId: roomId, - }, + } as unknown as RoomMember, }[userId] || null; }); }); @@ -222,11 +231,13 @@ describe("Room", function() { it("Make sure legacy overload passing options directly as parameters still works", () => { expect(() => room.addLiveEvents(events, DuplicateStrategy.Replace, false)).not.toThrow(); expect(() => room.addLiveEvents(events, DuplicateStrategy.Ignore, true)).not.toThrow(); + // @ts-ignore expect(() => room.addLiveEvents(events, "shouldfailbecauseinvalidduplicatestrategy", false)).toThrow(); }); it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function() { expect(function() { + // @ts-ignore room.addLiveEvents(events, { duplicateStrategy: "foo", }); @@ -255,6 +266,7 @@ describe("Room", function() { dupe.event.event_id = events[0].getId(); room.addLiveEvents(events); expect(room.timeline[0]).toEqual(events[0]); + // @ts-ignore room.addLiveEvents([dupe], { duplicateStrategy: "ignore", }); @@ -263,7 +275,7 @@ describe("Room", function() { it("should emit 'Room.timeline' events", function() { let callCount = 0; - room.on("Room.timeline", function(event, emitRoom, toStart) { + room.on(RoomEvent.Timeline, function(event, emitRoom, toStart) { callCount += 1; expect(room.timeline.length).toEqual(callCount); expect(event).toEqual(events[callCount - 1]); @@ -306,8 +318,8 @@ describe("Room", function() { userId: userA, membership: "join", name: "Alice", - }; - room.currentState.getSentinelMember.mockImplementation(function(uid) { + } as unknown as RoomMember; + mocked(room.currentState.getSentinelMember).mockImplementation(function(uid) { if (uid === userA) { return sentinel; } @@ -331,27 +343,25 @@ describe("Room", function() { const remoteEventId = remoteEvent.getId(); let callCount = 0; - room.on("Room.localEchoUpdated", - function(event, emitRoom, oldEventId, oldStatus) { - switch (callCount) { - case 0: - expect(event.getId()).toEqual(localEventId); - expect(event.status).toEqual(EventStatus.SENDING); - expect(emitRoom).toEqual(room); - expect(oldEventId).toBeUndefined(); - expect(oldStatus).toBeUndefined(); - break; - case 1: - expect(event.getId()).toEqual(remoteEventId); - expect(event.status).toBeNull(); - expect(emitRoom).toEqual(room); - expect(oldEventId).toEqual(localEventId); - expect(oldStatus).toBe(EventStatus.SENDING); - break; - } - callCount += 1; - }, - ); + room.on(RoomEvent.LocalEchoUpdated, (event, emitRoom, oldEventId, oldStatus) => { + switch (callCount) { + case 0: + expect(event.getId()).toEqual(localEventId); + expect(event.status).toEqual(EventStatus.SENDING); + expect(emitRoom).toEqual(room); + expect(oldEventId).toBeUndefined(); + expect(oldStatus).toBeUndefined(); + break; + case 1: + expect(event.getId()).toEqual(remoteEventId); + expect(event.status).toBeNull(); + expect(emitRoom).toEqual(room); + expect(oldEventId).toEqual(localEventId); + expect(oldStatus).toBe(EventStatus.SENDING); + break; + } + callCount += 1; + }); // first add the local echo room.addPendingEvent(localEvent, "TXN_ID"); @@ -367,7 +377,7 @@ describe("Room", function() { it("should be able to update local echo without a txn ID (/send then /sync)", function() { const eventJson = utils.mkMessage({ room: roomId, user: userA, event: false, - }) as object; + }); delete eventJson["txn_id"]; delete eventJson["event_id"]; const localEvent = new MatrixEvent(Object.assign({ event_id: "$temp" }, eventJson)); @@ -398,7 +408,7 @@ describe("Room", function() { it("should be able to update local echo without a txn ID (/sync then /send)", function() { const eventJson = utils.mkMessage({ room: roomId, user: userA, event: false, - }) as object; + }); delete eventJson["txn_id"]; delete eventJson["event_id"]; const txnId = "My_txn_id"; @@ -483,7 +493,7 @@ describe("Room", function() { it("should emit 'Room.timeline' events when added to the start", function() { let callCount = 0; - room.on("Room.timeline", function(event, emitRoom, toStart) { + room.on(RoomEvent.Timeline, function(event, emitRoom, toStart) { callCount += 1; expect(room.timeline.length).toEqual(callCount); expect(event).toEqual(events[callCount - 1]); @@ -501,19 +511,19 @@ describe("Room", function() { userId: userA, membership: "join", name: "Alice", - }; + } as unknown as RoomMember; const oldSentinel = { userId: userA, membership: "join", name: "Old Alice", - }; - room.currentState.getSentinelMember.mockImplementation(function(uid) { + } as unknown as RoomMember; + mocked(room.currentState.getSentinelMember).mockImplementation(function(uid) { if (uid === userA) { return sentinel; } return null; }); - room.oldState.getSentinelMember.mockImplementation(function(uid) { + mocked(room.oldState.getSentinelMember).mockImplementation(function(uid) { if (uid === userA) { return oldSentinel; } @@ -539,19 +549,19 @@ describe("Room", function() { userId: userA, membership: "join", name: "Alice", - }; + } as unknown as RoomMember; const oldSentinel = { userId: userA, membership: "join", name: "Old Alice", - }; - room.currentState.getSentinelMember.mockImplementation(function(uid) { + } as unknown as RoomMember; + mocked(room.currentState.getSentinelMember).mockImplementation(function(uid) { if (uid === userA) { return sentinel; } return null; }); - room.oldState.getSentinelMember.mockImplementation(function(uid) { + mocked(room.oldState.getSentinelMember).mockImplementation(function(uid) { if (uid === userA) { return oldSentinel; } @@ -599,7 +609,7 @@ describe("Room", function() { }); }); - const resetTimelineTests = function(timelineSupport) { + const resetTimelineTests = function(timelineSupport: boolean) { let events: MatrixEvent[]; beforeEach(function() { @@ -630,8 +640,8 @@ describe("Room", function() { const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS); const newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS); expect(room.getLiveTimeline().getEvents().length).toEqual(1); - expect(oldState.getStateEvents(EventType.RoomName, "")).toEqual(events[1]); - expect(newState.getStateEvents(EventType.RoomName, "")).toEqual(events[2]); + expect(oldState?.getStateEvents(EventType.RoomName, "")).toEqual(events[1]); + expect(newState?.getStateEvents(EventType.RoomName, "")).toEqual(events[2]); }); it("should reset the legacy timeline fields", function() { @@ -669,17 +679,14 @@ describe("Room", function() { expect(currentStateUpdateEmitCount).toEqual(timelineSupport ? 1 : 0); }); - it("should emit Room.timelineReset event and set the correct " + - "pagination token", function() { + it("should emit Room.timelineReset event and set the correct pagination token", function() { let callCount = 0; - room.on("Room.timelineReset", function(emitRoom) { + room.on(RoomEvent.TimelineReset, function(emitRoom) { callCount += 1; expect(emitRoom).toEqual(room); - // make sure that the pagination token has been set before the - // event is emitted. - const tok = emitRoom.getLiveTimeline() - .getPaginationToken(EventTimeline.BACKWARDS); + // make sure that the pagination token has been set before the event is emitted. + const tok = emitRoom?.getLiveTimeline().getPaginationToken(EventTimeline.BACKWARDS); expect(tok).toEqual("pagToken"); }); @@ -693,7 +700,7 @@ describe("Room", function() { const firstLiveTimeline = room.getLiveTimeline(); room.resetLiveTimeline('sometoken', 'someothertoken'); - const tl = room.getTimelineForEvent(events[0].getId()); + const tl = room.getTimelineForEvent(events[0].getId()!); expect(tl).toBe(timelineSupport ? firstLiveTimeline : null); }); }; @@ -721,30 +728,25 @@ describe("Room", function() { it("should handle events in the same timeline", function() { room.addLiveEvents(events); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, - events[1].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!)) .toBeLessThan(0); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId()!, - events[1].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId()!, events[1].getId()!)) .toBeGreaterThan(0); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, - events[1].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[1].getId()!)) .toEqual(0); }); it("should handle events in adjacent timelines", function() { const oldTimeline = room.addTimeline(); - oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), 'f'); - room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, 'b'); + oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), Direction.Forward); + room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, Direction.Backward); room.addEventsToTimeline([events[0]], false, oldTimeline); room.addLiveEvents([events[1]]); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, - events[1].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!)) .toBeLessThan(0); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, - events[0].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[0].getId()!)) .toBeGreaterThan(0); }); @@ -754,11 +756,9 @@ describe("Room", function() { room.addEventsToTimeline([events[0]], false, oldTimeline); room.addLiveEvents([events[1]]); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, - events[1].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!)) .toBe(null); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, - events[0].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[0].getId()!)) .toBe(null); }); @@ -769,21 +769,21 @@ describe("Room", function() { .compareEventOrdering(events[0].getId()!, "xxx")) .toBe(null); expect(room.getUnfilteredTimelineSet() - .compareEventOrdering("xxx", events[0].getId())) + .compareEventOrdering("xxx", events[0].getId()!)) .toBe(null); expect(room.getUnfilteredTimelineSet() - .compareEventOrdering(events[0].getId()!, events[0].getId())) + .compareEventOrdering(events[0].getId()!, events[0].getId()!)) .toBe(0); }); }); describe("getJoinedMembers", function() { it("should return members whose membership is 'join'", function() { - room.currentState.getMembers.mockImplementation(function() { + mocked(room.currentState.getMembers).mockImplementation(function() { return [ - { userId: "@alice:bar", membership: "join" }, - { userId: "@bob:bar", membership: "invite" }, - { userId: "@cleo:bar", membership: "leave" }, + { userId: "@alice:bar", membership: "join" } as unknown as RoomMember, + { userId: "@bob:bar", membership: "invite" } as unknown as RoomMember, + { userId: "@cleo:bar", membership: "leave" } as unknown as RoomMember, ]; }); const res = room.getJoinedMembers(); @@ -792,9 +792,9 @@ describe("Room", function() { }); it("should return an empty list if no membership is 'join'", function() { - room.currentState.getMembers.mockImplementation(function() { + mocked(room.currentState.getMembers).mockImplementation(function() { return [ - { userId: "@bob:bar", membership: "invite" }, + { userId: "@bob:bar", membership: "invite" } as unknown as RoomMember, ]; }); const res = room.getJoinedMembers(); @@ -805,41 +805,41 @@ describe("Room", function() { describe("hasMembershipState", function() { it("should return true for a matching userId and membership", function() { - room.currentState.getMember.mockImplementation(function(userId) { + mocked(room.currentState.getMember).mockImplementation(function(userId) { return { "@alice:bar": { userId: "@alice:bar", membership: "join" }, "@bob:bar": { userId: "@bob:bar", membership: "invite" }, - }[userId]; + }[userId] as unknown as RoomMember; }); expect(room.hasMembershipState("@bob:bar", "invite")).toBe(true); }); it("should return false if match membership but no match userId", function() { - room.currentState.getMember.mockImplementation(function(userId) { + mocked(room.currentState.getMember).mockImplementation(function(userId) { return { "@alice:bar": { userId: "@alice:bar", membership: "join" }, - }[userId]; + }[userId] as unknown as RoomMember; }); expect(room.hasMembershipState("@bob:bar", "join")).toBe(false); }); it("should return false if match userId but no match membership", function() { - room.currentState.getMember.mockImplementation(function(userId) { + mocked(room.currentState.getMember).mockImplementation(function(userId) { return { "@alice:bar": { userId: "@alice:bar", membership: "join" }, - }[userId]; + }[userId] as unknown as RoomMember; }); expect(room.hasMembershipState("@alice:bar", "ban")).toBe(false); }); it("should return false if no match membership or userId", function() { - room.currentState.getMember.mockImplementation(function(userId) { + mocked(room.currentState.getMember).mockImplementation(function(userId) { return { "@alice:bar": { userId: "@alice:bar", membership: "join" }, - }[userId]; + }[userId] as unknown as RoomMember; }); expect(room.hasMembershipState("@bob:bar", "invite")).toBe(false); }); @@ -1193,8 +1193,8 @@ describe("Room", function() { event: true, }); - function mkReceipt(roomId: string, records) { - const content = {}; + function mkReceipt(roomId: string, records: Array>) { + const content: IContent = {}; records.forEach(function(r) { if (!content[r.eventId]) { content[r.eventId] = {}; @@ -1241,7 +1241,7 @@ describe("Room", function() { it("should emit an event when a receipt is added", function() { const listener = jest.fn(); - room.on("Room.receipt", listener); + room.on(RoomEvent.Receipt, listener); const ts = 13787898424; @@ -1448,7 +1448,7 @@ describe("Room", function() { }); describe("tags", function() { - function mkTags(roomId, tags) { + function mkTags(roomId: string, tags: object) { const content = { "tags": tags }; return new MatrixEvent({ content: content, @@ -1470,7 +1470,7 @@ describe("Room", function() { "received on the event stream", function() { const listener = jest.fn(); - room.on("Room.tags", listener); + room.on(RoomEvent.Tags, listener); const tags = { "m.foo": { "order": 0.5 } }; const event = mkTags(roomId, tags); @@ -1642,11 +1642,14 @@ describe("Room", function() { }); describe("loadMembersIfNeeded", function() { - function createClientMock(serverResponse, storageResponse: MatrixEvent[] | Error | null = null) { + function createClientMock( + serverResponse: Error | MatrixEvent[], + storageResponse: MatrixEvent[] | Error | null = null, + ) { return { getEventMapper: function() { // events should already be MatrixEvents - return function(event) {return event;}; + return function(event: MatrixEvent) {return event;}; }, isCryptoEnabled() { return true; @@ -1671,7 +1674,7 @@ describe("Room", function() { return Promise.resolve(this.storageResponse); } }, - setOutOfBandMembers: function(roomId, memberEvents) { + setOutOfBandMembers: function(roomId: string, memberEvents: IStateEventWithRoomId[]) { this.storedMembers = memberEvents; return Promise.resolve(); }, @@ -2170,7 +2173,7 @@ describe("Room", function() { }, }); - room.createThread("$000", undefined, [eventWithoutARootEvent]); + room.createThread("$000", undefined, [eventWithoutARootEvent], false); const rootEvent = new MatrixEvent({ event_id: "$666", @@ -2188,7 +2191,7 @@ describe("Room", function() { }, }); - expect(() => room.createThread(rootEvent.getId()!, rootEvent, [])).not.toThrow(); + expect(() => room.createThread(rootEvent.getId()!, rootEvent, [], false)).not.toThrow(); }); it("creating thread from edited event should not conflate old versions of the event", () => { @@ -2406,8 +2409,6 @@ describe("Room", function() { Thread.setServerSideListSupport(FeatureSupport.Stable); room.client.createThreadListMessagesRequest = () => Promise.resolve({ - start: null, - end: null, chunk: [], state: [], }); @@ -2761,7 +2762,7 @@ describe("Room", function() { }); describe("thread notifications", () => { - let room; + let room: Room; beforeEach(() => { const client = new TestClient(userA).client; diff --git a/spec/unit/scheduler.spec.ts b/spec/unit/scheduler.spec.ts index 59c2d0a1d45..2e7dcf30898 100644 --- a/spec/unit/scheduler.spec.ts +++ b/spec/unit/scheduler.spec.ts @@ -1,18 +1,19 @@ // This file had a function whose name is all caps, which displeases eslint /* eslint new-cap: "off" */ -import { defer } from '../../src/utils'; +import { defer, IDeferred } from '../../src/utils'; import { MatrixError } from "../../src/http-api"; import { MatrixScheduler } from "../../src/scheduler"; import * as utils from "../test-utils/test-utils"; +import { MatrixEvent } from "../../src"; jest.useFakeTimers(); describe("MatrixScheduler", function() { - let scheduler; - let retryFn; - let queueFn; - let deferred; + let scheduler: MatrixScheduler>; + let retryFn: Function | null; + let queueFn: ((event: MatrixEvent) => string | null) | null; + let deferred: IDeferred>; const roomId = "!foo:bar"; const eventA = utils.mkMessage({ user: "@alice:bar", room: roomId, event: true, @@ -65,8 +66,8 @@ describe("MatrixScheduler", function() { deferB.resolve({ b: true }); deferA.resolve({ a: true }); const [a, b] = await abPromise; - expect(a.a).toEqual(true); - expect(b.b).toEqual(true); + expect(a!.a).toEqual(true); + expect(b!.b).toEqual(true); }); it("should invoke the retryFn on failure and wait the amount of time specified", @@ -92,6 +93,7 @@ describe("MatrixScheduler", function() { return new Promise(() => {}); } expect(procCount).toBeLessThan(3); + return new Promise(() => {}); }); scheduler.queueEvent(eventA); @@ -119,8 +121,8 @@ describe("MatrixScheduler", function() { return "yep"; }; - const deferA = defer(); - const deferB = defer(); + const deferA = defer>(); + const deferB = defer>(); let procCount = 0; scheduler.setProcessFunction(function(ev) { procCount += 1; @@ -132,6 +134,7 @@ describe("MatrixScheduler", function() { return deferB.promise; } expect(procCount).toBeLessThan(3); + return new Promise>(() => {}); }); const globalA = scheduler.queueEvent(eventA); @@ -159,7 +162,7 @@ describe("MatrixScheduler", function() { const eventC = utils.mkMessage({ user: "@a:bar", room: roomId, event: true }); const eventD = utils.mkMessage({ user: "@b:bar", room: roomId, event: true }); - const buckets = {}; + const buckets: Record = {}; buckets[eventA.getId()!] = "queue_A"; buckets[eventD.getId()!] = "queue_A"; buckets[eventB.getId()!] = "queue_B"; @@ -169,13 +172,13 @@ describe("MatrixScheduler", function() { return 0; }; queueFn = function(event) { - return buckets[event.getId()]; + return buckets[event.getId()!]; }; const expectOrder = [ eventA.getId(), eventB.getId(), eventD.getId(), ]; - const deferA = defer(); + const deferA = defer>(); scheduler.setProcessFunction(function(event) { const id = expectOrder.shift(); expect(id).toEqual(event.getId()); @@ -191,7 +194,7 @@ describe("MatrixScheduler", function() { // wait a bit then resolve A and we should get D (not C) next. setTimeout(function() { - deferA.resolve(); + deferA.resolve({}); }, 1000); jest.advanceTimersByTime(1000); }); @@ -210,7 +213,7 @@ describe("MatrixScheduler", function() { }; const prom = scheduler.queueEvent(eventA); expect(prom).toBeTruthy(); - expect(prom.then).toBeTruthy(); + expect(prom!.then).toBeTruthy(); }); }); @@ -237,15 +240,15 @@ describe("MatrixScheduler", function() { scheduler.queueEvent(eventA); scheduler.queueEvent(eventB); const queue = scheduler.getQueueForEvent(eventA); - expect(queue.length).toEqual(2); + expect(queue).toHaveLength(2); expect(queue).toEqual([eventA, eventB]); // modify the queue const eventC = utils.mkMessage( { user: "@a:bar", room: roomId, event: true }, ); - queue.push(eventC); + queue!.push(eventC); const queueAgain = scheduler.getQueueForEvent(eventA); - expect(queueAgain.length).toEqual(2); + expect(queueAgain).toHaveLength(2); }); it("should return a list of events in the queue and modifications to" + @@ -255,10 +258,10 @@ describe("MatrixScheduler", function() { }; scheduler.queueEvent(eventA); scheduler.queueEvent(eventB); - const queue = scheduler.getQueueForEvent(eventA); - queue[1].event.content.body = "foo"; - const queueAgain = scheduler.getQueueForEvent(eventA); - expect(queueAgain[1].event.content.body).toEqual("foo"); + const queue = scheduler.getQueueForEvent(eventA)!; + queue[1].event.content!.body = "foo"; + const queueAgain = scheduler.getQueueForEvent(eventA)!; + expect(queueAgain[1].event.content?.body).toEqual("foo"); }); }); diff --git a/spec/unit/sync-accumulator.spec.ts b/spec/unit/sync-accumulator.spec.ts index 2e1ec58b60c..9a72a6b3397 100644 --- a/spec/unit/sync-accumulator.spec.ts +++ b/spec/unit/sync-accumulator.spec.ts @@ -16,7 +16,8 @@ limitations under the License. */ import { ReceiptType } from "../../src/@types/read_receipts"; -import { SyncAccumulator } from "../../src/sync-accumulator"; +import { IJoinedRoom, ISyncResponse, SyncAccumulator } from "../../src/sync-accumulator"; +import { IRoomSummary } from "../../src"; // The event body & unsigned object get frozen to assert that they don't get altered // by the impl @@ -55,10 +56,10 @@ const RES_WITH_AGE = { }, }, }, -}; +} as unknown as ISyncResponse; describe("SyncAccumulator", function() { - let sa; + let sa: SyncAccumulator; beforeEach(function() { sa = new SyncAccumulator({ @@ -98,7 +99,7 @@ describe("SyncAccumulator", function() { }, }, }, - }; + } as unknown as ISyncResponse; sa.accumulate(res); const output = sa.getJSON(); expect(output.nextBatch).toEqual(res.next_batch); @@ -223,7 +224,6 @@ describe("SyncAccumulator", function() { content: { user_ids: ["@alice:localhost"], }, - room_id: "!foo:bar", }], }, }); @@ -281,12 +281,12 @@ describe("SyncAccumulator", function() { account_data: { events: [acc1], }, - }); + } as unknown as ISyncResponse); sa.accumulate({ account_data: { events: [acc2], }, - }); + } as unknown as ISyncResponse); expect( sa.getJSON().accountData.length, ).toEqual(1); @@ -422,7 +422,7 @@ describe("SyncAccumulator", function() { }); describe("summary field", function() { - function createSyncResponseWithSummary(summary) { + function createSyncResponseWithSummary(summary: IRoomSummary): ISyncResponse { return { next_batch: "abc", rooms: { @@ -444,7 +444,7 @@ describe("SyncAccumulator", function() { }, }, }, - }; + } as unknown as ISyncResponse; } afterEach(() => { @@ -487,8 +487,8 @@ describe("SyncAccumulator", function() { jest.spyOn(global.Date, 'now').mockReturnValue(startingTs + delta); const output = sa.getJSON(); - expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned.age).toEqual( - RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned.age + delta, + expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned?.age).toEqual( + RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned!.age! + delta, ); expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual( Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]), @@ -507,12 +507,12 @@ describe("SyncAccumulator", function() { sa.accumulate(RES_WITH_AGE); const output = sa.getJSON(); expect(output.roomsData.join["!foo:bar"] - .unread_thread_notifications["$143273582443PhrSn:example.org"]).not.toBeUndefined(); + .unread_thread_notifications!["$143273582443PhrSn:example.org"]).not.toBeUndefined(); }); }); }); -function syncSkeleton(joinObj) { +function syncSkeleton(joinObj: Partial): ISyncResponse { joinObj = joinObj || {}; return { next_batch: "abc", @@ -521,11 +521,12 @@ function syncSkeleton(joinObj) { "!foo:bar": joinObj, }, }, - }; + } as unknown as ISyncResponse; } -function msg(localpart, text) { +function msg(localpart: string, text: string) { return { + event_id: "$" + Math.random(), content: { body: text, }, @@ -535,8 +536,9 @@ function msg(localpart, text) { }; } -function member(localpart, membership) { +function member(localpart: string, membership: string) { return { + event_id: "$" + Math.random(), content: { membership: membership, }, diff --git a/spec/unit/utils.spec.ts b/spec/unit/utils.spec.ts index 1b11f2a7cd3..c9fc75c3e85 100644 --- a/spec/unit/utils.spec.ts +++ b/spec/unit/utils.spec.ts @@ -455,7 +455,11 @@ describe("utils", function() { describe("recursivelyAssign", () => { it("doesn't override with null/undefined", () => { - const result = utils.recursivelyAssign( + const result = utils.recursivelyAssign<{ + string: string; + object: object; + float: number; + }, {}>( { string: "Hello world", object: {}, @@ -475,7 +479,11 @@ describe("utils", function() { }); it("assigns recursively", () => { - const result = utils.recursivelyAssign( + const result = utils.recursivelyAssign<{ + number: number; + object: object; + thing: string | object; + }, {}>( { number: 42, object: { diff --git a/spec/unit/webrtc/call.spec.ts b/spec/unit/webrtc/call.spec.ts index d1cb5d29616..1d4f9f03401 100644 --- a/spec/unit/webrtc/call.spec.ts +++ b/spec/unit/webrtc/call.spec.ts @@ -933,7 +933,7 @@ describe('Call', function() { await fakeIncomingCall(client, call, "1"); }); - const untilEventSent = async (...args) => { + const untilEventSent = async (...args: any[]) => { const maxTries = 20; for (let tries = 0; tries < maxTries; ++tries) { @@ -971,7 +971,7 @@ describe('Call', function() { }); describe("ICE candidate sending", () => { - let mockPeerConn; + let mockPeerConn: MockRTCPeerConnection; const fakeCandidateString = "here is a fake candidate!"; const fakeCandidateEvent = { candidate: { @@ -1086,7 +1086,7 @@ describe('Call', function() { }); describe("Screen sharing", () => { - const waitNegotiateFunc = resolve => { + const waitNegotiateFunc = (resolve: Function): void => { mockSendEvent.mockImplementationOnce(() => { // Note that the peer connection here is a dummy one and always returns // dummy SDP, so there's not much point returning the content: the SDP will diff --git a/src/@types/read_receipts.ts b/src/@types/read_receipts.ts index 689313672a3..ea832926072 100644 --- a/src/@types/read_receipts.ts +++ b/src/@types/read_receipts.ts @@ -42,7 +42,7 @@ export type ReceiptCache = {[eventId: string]: CachedReceipt[]}; export interface ReceiptContent { [eventId: string]: { - [key in ReceiptType]: { + [key in ReceiptType | string]: { [userId: string]: Receipt; }; }; diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index d26e4d5c683..fe2b6e383f3 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -49,7 +49,7 @@ interface WellKnownConfig extends Omit { error?: IWellKnownConfig["error"] | null; } -interface ClientConfig { +interface ClientConfig extends Omit { "m.homeserver": WellKnownConfig; "m.identity_server": WellKnownConfig; } @@ -131,7 +131,7 @@ export class AutoDiscovery { * configuration, which may include error states. Rejects on unexpected * failure, not when verification fails. */ - public static async fromDiscoveryConfig(wellknown: any): Promise { + public static async fromDiscoveryConfig(wellknown: IClientWellKnown): Promise { // Step 1 is to get the config, which is provided to us here. // We default to an error state to make the first few checks easier to @@ -185,7 +185,7 @@ export class AutoDiscovery { const hsVersions = await this.fetchWellKnownObject( `${hsUrl}/_matrix/client/versions`, ); - if (!hsVersions || !hsVersions.raw["versions"]) { + if (!hsVersions || !hsVersions.raw?.["versions"]) { logger.error("Invalid /versions response"); clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER; @@ -219,9 +219,7 @@ export class AutoDiscovery { // Step 5a: Make sure the URL is valid *looking*. We'll make sure it // points to an identity server in Step 5b. - isUrl = this.sanitizeWellKnownUrl( - wellknown["m.identity_server"]["base_url"], - ); + isUrl = this.sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]); if (!isUrl) { logger.error("Invalid base_url for m.identity_server"); failingClientConfig["m.identity_server"].error = @@ -234,7 +232,7 @@ export class AutoDiscovery { const isResponse = await this.fetchWellKnownObject( `${isUrl}/_matrix/identity/api/v1`, ); - if (!isResponse || !isResponse.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) { + if (!isResponse?.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) { logger.error("Invalid /api/v1 response"); failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; @@ -259,14 +257,16 @@ export class AutoDiscovery { // Step 7: Copy any other keys directly into the clientConfig. This is for // things like custom configuration of services. - Object.keys(wellknown).forEach((k) => { + Object.keys(wellknown).forEach((k: keyof IClientWellKnown) => { if (k === "m.homeserver" || k === "m.identity_server") { // Only copy selected parts of the config to avoid overwriting // properties computed by the validation logic above. const notProps = ["error", "state", "base_url"]; - for (const prop of Object.keys(wellknown[k])) { + for (const prop of Object.keys(wellknown[k]!)) { if (notProps.includes(prop)) continue; - clientConfig[k][prop] = wellknown[k][prop]; + type Prop = Exclude; + // @ts-ignore - ts gets unhappy as we're mixing types here + clientConfig[k][prop as Prop] = wellknown[k]![prop as Prop]; } } else { // Just copy the whole thing over otherwise @@ -347,7 +347,7 @@ export class AutoDiscovery { } // Step 2: Validate and parse the config - return AutoDiscovery.fromDiscoveryConfig(wellknown.raw); + return AutoDiscovery.fromDiscoveryConfig(wellknown.raw!); } /** @@ -378,7 +378,7 @@ export class AutoDiscovery { * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid. * @private */ - private static sanitizeWellKnownUrl(url: string): string | false { + private static sanitizeWellKnownUrl(url?: string | null): string | false { if (!url) return false; try { diff --git a/src/browser-index.js b/src/browser-index.ts similarity index 76% rename from src/browser-index.js rename to src/browser-index.ts index 86e887bd49f..200b2a32d7c 100644 --- a/src/browser-index.js +++ b/src/browser-index.ts @@ -16,27 +16,28 @@ limitations under the License. import * as matrixcs from "./matrix"; +type BrowserMatrix = typeof matrixcs; +declare global { + /* eslint-disable no-var, camelcase */ + var __js_sdk_entrypoint: boolean; + var matrixcs: BrowserMatrix; + /* eslint-enable no-var */ +} + if (global.__js_sdk_entrypoint) { throw new Error("Multiple matrix-js-sdk entrypoints detected!"); } global.__js_sdk_entrypoint = true; -// just *accessing* indexedDB throws an exception in firefox with -// indexeddb disabled. -let indexedDB; +// just *accessing* indexedDB throws an exception in firefox with indexeddb disabled. +let indexedDB: IDBFactory | undefined; try { indexedDB = global.indexedDB; } catch (e) {} // if our browser (appears to) support indexeddb, use an indexeddb crypto store. if (indexedDB) { - matrixcs.setCryptoStoreFactory( - function() { - return new matrixcs.IndexedDBCryptoStore( - indexedDB, "matrix-js-sdk:crypto", - ); - }, - ); + matrixcs.setCryptoStoreFactory(() => new matrixcs.IndexedDBCryptoStore(indexedDB!, "matrix-js-sdk:crypto")); } // We export 3 things to make browserify happy as well as downstream projects. diff --git a/src/client.ts b/src/client.ts index 9abcd967f70..fae73b7a4e4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -530,7 +530,7 @@ export interface IPreviewUrlResponse { "matrix:image:size"?: number; } -interface ITurnServerResponse { +export interface ITurnServerResponse { uris: string[]; username: string; password: string; @@ -561,7 +561,7 @@ export interface IClientWellKnown { } export interface IWellKnownConfig { - raw?: any; // todo typings + raw?: IClientWellKnown; action?: AutoDiscoveryAction; reason?: string; error?: Error | string; @@ -605,10 +605,10 @@ interface ITagMetadata { } interface IMessagesResponse { - start: string; - end: string; + start?: string; + end?: string; chunk: IRoomEvent[]; - state: IStateEvent[]; + state?: IStateEvent[]; } interface IThreadedMessagesResponse { @@ -635,6 +635,17 @@ export interface IUploadKeysRequest { "org.matrix.msc2732.fallback_keys"?: Record; } +export interface IQueryKeysRequest { + device_keys: { [userId: string]: string[] }; + timeout?: number; + token?: string; +} + +export interface IClaimKeysRequest { + one_time_keys: { [userId: string]: { [deviceId: string]: string } }; + timeout?: number; +} + export interface IOpenIDToken { access_token: string; token_type: "Bearer" | string; @@ -1744,13 +1755,15 @@ export class MatrixClient extends TypedEventEmitter(Method.Get, "/capabilities").catch((e: Error): void => { + }; + return this.http.authedRequest(Method.Get, "/capabilities").catch((e: Error): Response => { // We swallow errors because we need a default object anyhow logger.error(e); + return {}; }).then((r = {}) => { - const capabilities: ICapabilities = r["capabilities"] || {}; + const capabilities = r["capabilities"] || {}; // If the capabilities missed the cache, cache it for a shorter amount // of time to try and refresh them later. @@ -3229,28 +3242,28 @@ export class MatrixClient extends TypedEventEmitter; public restoreKeyBackupWithRecoveryKey( recoveryKey: string, targetRoomId: string, targetSessionId: undefined, backupInfo: IKeyBackupInfo, - opts: IKeyBackupRestoreOpts, + opts?: IKeyBackupRestoreOpts, ): Promise; public restoreKeyBackupWithRecoveryKey( recoveryKey: string, targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupInfo, - opts: IKeyBackupRestoreOpts, + opts?: IKeyBackupRestoreOpts, ): Promise; public restoreKeyBackupWithRecoveryKey( recoveryKey: string, targetRoomId: string | undefined, targetSessionId: string | undefined, backupInfo: IKeyBackupInfo, - opts: IKeyBackupRestoreOpts, + opts?: IKeyBackupRestoreOpts, ): Promise { const privKey = decodeRecoveryKey(recoveryKey); return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts); @@ -3442,7 +3455,7 @@ export class MatrixClient extends TypedEventEmitter = {}; for (const [userId, devices] of Object.entries(deviceInfos)) { devicesByUser[userId] = Object.values(devices); } @@ -3616,7 +3629,7 @@ export class MatrixClient extends TypedEventEmitter { - const content = { ignored_users: {} }; + const content = { ignored_users: {} as Record }; userIds.forEach((u) => { content.ignored_users[u] = {}; }); @@ -3910,16 +3923,25 @@ export class MatrixClient extends TypedEventEmitter; public sendEvent( roomId: string, - threadId: string | null, - eventType: string | IContent, - content?: IContent | string, - txnId?: string, + threadIdOrEventType: string | null, + eventTypeOrContent: string | IContent, + contentOrTxnId?: IContent | string, + txnIdOrVoid?: string, ): Promise { - if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - txnId = content as string; - content = eventType as IContent; - eventType = threadId; + let threadId: string | null; + let eventType: string; + let content: IContent; + let txnId: string | undefined; + if (!threadIdOrEventType?.startsWith(EVENT_ID_PREFIX) && threadIdOrEventType !== null) { + txnId = contentOrTxnId as string; + content = eventTypeOrContent as IContent; + eventType = threadIdOrEventType; threadId = null; + } else { + txnId = txnIdOrVoid; + content = contentOrTxnId as IContent; + eventType = eventTypeOrContent as string; + threadId = threadIdOrEventType; } // If we expect that an event is part of a thread but is missing the relation @@ -4280,7 +4302,7 @@ export class MatrixClient extends TypedEventEmitter | undefined => { - let newEvent: IPartialEvent | undefined; + let newEvent: IPartialEvent | undefined; if (content['msgtype'] === MsgType.Text) { newEvent = MessageEvent.from(content['body'], content['formatted_body']).serialize(); @@ -5262,11 +5284,11 @@ export class MatrixClient extends TypedEventEmitter { @@ -6261,7 +6283,7 @@ export class MatrixClient extends TypedEventEmitter | undefined { + public setRoomMutePushRule(scope: "global" | "device", roomId: string, mute: boolean): Promise | undefined { let promise: Promise | undefined; let hasDontNotifyRule = false; @@ -6818,7 +6840,7 @@ export class MatrixClient extends TypedEventEmitter { return primTypes.includes(typeof value); }) - .reduce((obj, [key, value]) => { + .reduce>((obj, [key, value]) => { obj[key] = value; return obj; }, {}); @@ -7841,7 +7863,7 @@ export class MatrixClient extends TypedEventEmitter { - const content: any = { + const content: IQueryKeysRequest = { device_keys: {}, }; if ('token' in opts) { @@ -8582,7 +8604,7 @@ export class MatrixClient extends TypedEventEmitter(Method.Post, "/lookup", params, IdentityPrefix.V2, identityAccessToken); - if (!response || !response['mappings']) return []; // no results + if (!response?.['mappings']) return []; // no results const foundAddresses: { address: string, mxid: string }[] = []; for (const hashed of Object.keys(response['mappings'])) { @@ -9028,7 +9049,7 @@ export class MatrixClient extends TypedEventEmitter { + const targets = Object.keys(contentMap).reduce>((obj, key) => { obj[key] = Object.keys(contentMap[key]); return obj; }, {}); diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index e776b93ad94..4035db04cf7 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -84,6 +84,7 @@ export class CrossSigningInfo { const res = new CrossSigningInfo(userId); for (const prop in obj) { if (obj.hasOwnProperty(prop)) { + // @ts-ignore - ts doesn't like this and nor should we res[prop] = obj[prop]; } } diff --git a/src/crypto/EncryptionSetup.ts b/src/crypto/EncryptionSetup.ts index 253c83c1d64..a46afdc4df6 100644 --- a/src/crypto/EncryptionSetup.ts +++ b/src/crypto/EncryptionSetup.ts @@ -35,7 +35,7 @@ import { IAccountDataClient } from "./SecretStorage"; interface ICrossSigningKeys { authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"]; - keys: Record; + keys: Record<"master" | "self_signing" | "user_signing", ICrossSigningKey>; } /** @@ -209,7 +209,7 @@ export class EncryptionSetupOperation { if (this.crossSigningKeys) { const keys: Partial = {}; for (const [name, key] of Object.entries(this.crossSigningKeys.keys)) { - keys[name + "_key"] = key; + keys[((name as keyof ICrossSigningKeys["keys"]) + "_key" as keyof CrossSigningKeys)] = key; } // We must only call `uploadDeviceSigningKeys` from inside this auth diff --git a/src/crypto/OlmDevice.ts b/src/crypto/OlmDevice.ts index efa34ad4159..13463ef115c 100644 --- a/src/crypto/OlmDevice.ts +++ b/src/crypto/OlmDevice.ts @@ -23,11 +23,19 @@ import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base"; import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm"; import { IMegolmSessionData } from "./index"; import { OlmGroupSessionExtraData } from "../@types/crypto"; +import { IMessage } from "./algorithms/olm"; // The maximum size of an event is 65K, and we base64 the content, so this is a // reasonable approximation to the biggest plaintext we can encrypt. const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4; +export class PayloadTooLargeError extends Error { + public readonly data = { + errcode: "M_TOO_LARGE", + error: "Payload too large for encrypted message", + }; +} + function checkPayloadLength(payloadString: string): void { if (payloadString === undefined) { throw new Error("payloadString undefined"); @@ -40,15 +48,8 @@ function checkPayloadLength(payloadString: string): void { // Note that even if we manage to do the encryption, the message send may fail, // because by the time we've wrapped the ciphertext in the event object, it may // exceed 65K. But at least we won't just fail with "abort()" in that case. - const err = new Error("Message too long (" + payloadString.length + " bytes). " + - "The maximum for an encrypted message is " + - MAX_PLAINTEXT_LENGTH + " bytes."); - // TODO: [TypeScript] We should have our own error types - err["data"] = { - errcode: "M_TOO_LARGE", - error: "Payload too large for encrypted message", - }; - throw err; + throw new PayloadTooLargeError(`Message too long (${payloadString.length} bytes). ` + + `The maximum for an encrypted message is ${MAX_PLAINTEXT_LENGTH} bytes.`); } } @@ -126,6 +127,8 @@ interface IInboundGroupSessionKey { } /* eslint-enable camelcase */ +type OneTimeKeys = { curve25519: { [keyId: string]: string } }; + /** * Manages the olm cryptography functions. Each OlmDevice has a single * OlmAccount and a number of OlmSessions. @@ -452,7 +455,7 @@ export class OlmDevice { * @return {Promise} base64-encoded signature */ public async sign(message: string): Promise { - let result; + let result: string; await this.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { @@ -460,7 +463,7 @@ export class OlmDevice { result = account.sign(message); }); }); - return result; + return result!; } /** @@ -470,8 +473,8 @@ export class OlmDevice { * curve25519, which is itself an object mapping key id to Curve25519 * key. */ - public async getOneTimeKeys(): Promise<{ curve25519: { [keyId: string]: string } }> { - let result; + public async getOneTimeKeys(): Promise { + let result: OneTimeKeys; await this.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { @@ -481,7 +484,7 @@ export class OlmDevice { }, ); - return result; + return result!; } /** @@ -821,10 +824,10 @@ export class OlmDevice { theirDeviceIdentityKey: string, sessionId: string, payloadString: string, - ): Promise { + ): Promise { checkPayloadLength(payloadString); - let res; + let res: IMessage; await this.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { @@ -840,7 +843,7 @@ export class OlmDevice { }, logger.withPrefix("[encryptMessage]"), ); - return res; + return res!; } /** @@ -860,7 +863,7 @@ export class OlmDevice { messageType: number, ciphertext: string, ): Promise { - let payloadString; + let payloadString: string; await this.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { @@ -877,7 +880,7 @@ export class OlmDevice { }, logger.withPrefix("[decryptMessage]"), ); - return payloadString; + return payloadString!; } /** @@ -902,7 +905,7 @@ export class OlmDevice { return false; } - let matches; + let matches: boolean; await this.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { @@ -912,7 +915,7 @@ export class OlmDevice { }, logger.withPrefix("[matchesSession]"), ); - return matches; + return matches!; } public async recordSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise { @@ -1293,7 +1296,7 @@ export class OlmDevice { result = null; return; } - let res; + let res: ReturnType; try { res = session.decrypt(body); } catch (e) { @@ -1313,8 +1316,8 @@ export class OlmDevice { let plaintext: string = res.plaintext; if (plaintext === undefined) { - // Compatibility for older olm versions. - plaintext = res; + // @ts-ignore - Compatibility for older olm versions. + plaintext = res as string; } else { // Check if we have seen this message index before to detect replay attacks. // If the event ID and timestamp are specified, and the match the event ID @@ -1555,7 +1558,7 @@ export class OlmDevice { } } -export const WITHHELD_MESSAGES = { +export const WITHHELD_MESSAGES: Record = { "m.unverified": "The sender has disabled encrypting to unverified devices.", "m.blacklisted": "The sender has blocked you.", "m.unauthorised": "You are not authorised to read the message.", diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts index a7e1e7d738f..54702034f46 100644 --- a/src/crypto/SecretStorage.ts +++ b/src/crypto/SecretStorage.ts @@ -18,10 +18,9 @@ import { v4 as uuidv4 } from 'uuid'; import { logger } from '../logger'; import * as olmlib from './olmlib'; -import { encodeBase64 } from './olmlib'; import { randomString } from '../randomstring'; import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes'; -import { ICryptoCallbacks } from "."; +import { ICryptoCallbacks, IEncryptedContent } from "."; import { IContent, MatrixEvent } from "../models/event"; import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client"; import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api'; @@ -61,8 +60,7 @@ interface IDecryptors { interface ISecretInfo { encrypted: { - // eslint-disable-next-line camelcase - key_id: IEncryptedPayload; + [keyId: string]: IEncryptedPayload; }; } @@ -318,23 +316,11 @@ export class SecretStorage { `the keys it is encrypted with are for a supported algorithm`); } - let keyId: string; - let decryption; - try { - // fetch private key from app - [keyId, decryption] = await this.getSecretStorageKey(keys, name); + // fetch private key from app + const [keyId, decryption] = await this.getSecretStorageKey(keys, name); + const encInfo = secretInfo.encrypted[keyId]; - const encInfo = secretInfo.encrypted[keyId]; - - // We don't actually need the decryption object if it's a passthrough - // since we just want to return the key itself. It must be base64 - // encoded, since this is how a key would normally be stored. - if (encInfo.passthrough) return encodeBase64(decryption.get_private_key()); - - return decryption.decrypt(encInfo); - } finally { - if (decryption && decryption.free) decryption.free(); - } + return decryption.decrypt(encInfo); } /** @@ -351,7 +337,7 @@ export class SecretStorage { const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); if (!secretInfo?.encrypted) return null; - const ret = {}; + const ret: Record = {}; // filter secret encryption keys with supported algorithm for (const keyId of Object.keys(secretInfo.encrypted)) { @@ -391,7 +377,7 @@ export class SecretStorage { requesting_device_id: this.baseApis.deviceId, request_id: requestId, }; - const toDevice = {}; + const toDevice: Record = {}; for (const device of devices) { toDevice[device] = cancelData; } @@ -412,7 +398,7 @@ export class SecretStorage { request_id: requestId, [ToDeviceMessageId]: uuidv4(), }; - const toDevice = {}; + const toDevice: Record = {}; for (const device of devices) { toDevice[device] = requestData; } @@ -490,9 +476,9 @@ export class SecretStorage { secret: secret, }, }; - const encryptedContent = { + const encryptedContent: IEncryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key, + sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key!, ciphertext: {}, [ToDeviceMessageId]: uuidv4(), }; diff --git a/src/crypto/algorithms/base.ts b/src/crypto/algorithms/base.ts index d6c70bc1067..32b9ab73b7e 100644 --- a/src/crypto/algorithms/base.ts +++ b/src/crypto/algorithms/base.ts @@ -23,8 +23,14 @@ limitations under the License. import { MatrixClient } from "../../client"; import { Room } from "../../models/room"; import { OlmDevice } from "../OlmDevice"; -import { MatrixEvent, RoomMember } from "../../matrix"; -import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from ".."; +import { IContent, MatrixEvent, RoomMember } from "../../matrix"; +import { + Crypto, + IEncryptedContent, + IEventDecryptionResult, + IMegolmSessionData, + IncomingRoomKeyRequest, +} from ".."; import { DeviceInfo } from "../deviceinfo"; import { IRoomEncryption } from "../RoomList"; @@ -108,7 +114,7 @@ export abstract class EncryptionAlgorithm { * * @return {Promise} Promise which resolves to the new event body */ - public abstract encryptMessage(room: Room, eventType: string, content: object): Promise; + public abstract encryptMessage(room: Room, eventType: string, content: IContent): Promise; /** * Called when the membership of a member of the room changes. diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index 9ba33e32a78..b099ef40ae6 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -38,9 +38,15 @@ import { Room } from '../../models/room'; import { DeviceInfo } from "../deviceinfo"; import { IOlmSessionResult } from "../olmlib"; import { DeviceInfoMap } from "../DeviceList"; -import { MatrixEvent } from "../../models/event"; +import { IContent, MatrixEvent } from "../../models/event"; import { EventType, MsgType, ToDeviceMessageId } from '../../@types/event'; -import { IEncryptedContent, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index"; +import { + IMegolmEncryptedContent, + IEventDecryptionResult, + IMegolmSessionData, + IncomingRoomKeyRequest, + IEncryptedContent, +} from "../index"; import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager'; import { OlmGroupSessionExtraData } from "../../@types/crypto"; import { MatrixError } from "../../http-api"; @@ -228,7 +234,7 @@ class OutboundSessionInfo { * @param {object} params parameters, as per * {@link module:crypto/algorithms/EncryptionAlgorithm} */ -class MegolmEncryption extends EncryptionAlgorithm { +export class MegolmEncryption extends EncryptionAlgorithm { // the most recent attempt to set up a session. This is used to serialise // the session setups, so that we have a race-free view of which session we // are using, and which devices we have shared the keys with. It resolves @@ -761,9 +767,9 @@ class MegolmEncryption extends EncryptionAlgorithm { }, }; - const encryptedContent = { + const encryptedContent: IEncryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, + sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: {}, [ToDeviceMessageId]: uuidv4(), }; @@ -1010,7 +1016,7 @@ class MegolmEncryption extends EncryptionAlgorithm { * * @return {Promise} Promise which resolves to the new event body */ - public async encryptMessage(room: Room, eventType: string, content: object): Promise { + public async encryptMessage(room: Room, eventType: string, content: IContent): Promise { logger.log(`Starting to encrypt event for ${this.roomId}`); if (this.encryptionPreparation != null) { @@ -1045,12 +1051,10 @@ class MegolmEncryption extends EncryptionAlgorithm { content: content, }; - const ciphertext = this.olmDevice.encryptGroupMessage( - session.sessionId, JSON.stringify(payloadJson), - ); - const encryptedContent = { + const ciphertext = this.olmDevice.encryptGroupMessage(session.sessionId, JSON.stringify(payloadJson)); + const encryptedContent: IEncryptedContent = { algorithm: olmlib.MEGOLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, + sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: ciphertext, session_id: session.sessionId, // Include our device ID so that recipients can send us a @@ -1064,7 +1068,7 @@ class MegolmEncryption extends EncryptionAlgorithm { return encryptedContent; } - private isVerificationEvent(eventType: string, content: object): boolean { + private isVerificationEvent(eventType: string, content: IContent): boolean { switch (eventType) { case EventType.KeyVerificationCancel: case EventType.KeyVerificationDone: @@ -1227,7 +1231,7 @@ class MegolmEncryption extends EncryptionAlgorithm { * @param {object} params parameters, as per * {@link module:crypto/algorithms/DecryptionAlgorithm} */ -class MegolmDecryption extends DecryptionAlgorithm { +export class MegolmDecryption extends DecryptionAlgorithm { // events which we couldn't decrypt due to unknown sessions / // indexes, or which we could only decrypt with untrusted keys: // map from senderKey|sessionId to Set of MatrixEvents @@ -1670,9 +1674,9 @@ class MegolmDecryption extends DecryptionAlgorithm { await olmlib.ensureOlmSessionsForDevices( this.olmDevice, this.baseApis, { [sender]: [device] }, false, ); - const encryptedContent = { + const encryptedContent: IEncryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, + sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: {}, [ToDeviceMessageId]: uuidv4(), }; @@ -1752,9 +1756,9 @@ class MegolmDecryption extends DecryptionAlgorithm { body.room_id, body.sender_key, body.session_id, ); }).then((payload) => { - const encryptedContent = { + const encryptedContent: IEncryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, + sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: {}, [ToDeviceMessageId]: uuidv4(), }; diff --git a/src/crypto/algorithms/olm.ts b/src/crypto/algorithms/olm.ts index 682aa4c7c87..114368319b7 100644 --- a/src/crypto/algorithms/olm.ts +++ b/src/crypto/algorithms/olm.ts @@ -30,13 +30,13 @@ import { registerAlgorithm, } from "./base"; import { Room } from '../../models/room'; -import { MatrixEvent } from "../../models/event"; -import { IEventDecryptionResult } from "../index"; +import { IContent, MatrixEvent } from "../../models/event"; +import { IEncryptedContent, IEventDecryptionResult, IOlmEncryptedContent } from "../index"; import { IInboundSession } from "../OlmDevice"; const DeviceVerification = DeviceInfo.DeviceVerification; -interface IMessage { +export interface IMessage { type: number; body: string; } @@ -91,7 +91,7 @@ class OlmEncryption extends EncryptionAlgorithm { * * @return {Promise} Promise which resolves to the new event body */ - public async encryptMessage(room: Room, eventType: string, content: object): Promise { + public async encryptMessage(room: Room, eventType: string, content: IContent): Promise { // pick the list of recipients based on the membership list. // // TODO: there is a race condition here! What if a new user turns up @@ -111,9 +111,9 @@ class OlmEncryption extends EncryptionAlgorithm { content: content, }; - const encryptedContent = { + const encryptedContent: IEncryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, + sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: {}, }; diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index f2160165bd8..51912893605 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -93,7 +93,7 @@ interface BackupAlgorithmClass { interface BackupAlgorithm { untrusted: boolean; - encryptSession(data: Record): Promise; + encryptSession(data: Record): Promise; decryptSessions(ciphertexts: Record): Promise; authData: AuthData; keyMatches(key: ArrayLike): Promise; @@ -663,7 +663,7 @@ export class Curve25519 implements BackupAlgorithm { public get untrusted(): boolean { return true; } - public async encryptSession(data: Record): Promise { + public async encryptSession(data: Record): Promise { const plainText: Record = Object.assign({}, data); delete plainText.session_id; delete plainText.room_id; @@ -788,7 +788,7 @@ export class Aes256 implements BackupAlgorithm { public get untrusted(): boolean { return false; } - public encryptSession(data: Record): Promise { + public encryptSession(data: Record): Promise { const plainText: Record = Object.assign({}, data); delete plainText.session_id; delete plainText.room_id; diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index cc6b252da59..1ed16b74cbd 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -258,7 +258,7 @@ export class DehydrationManager { } logger.log("Preparing fallback keys"); - const fallbackKeys = {}; + const fallbackKeys: Record = {}; for (const [keyId, key] of Object.entries(fallbacks.curve25519)) { const k: IOneTimeKey = { key, fallback: true }; const signature = account.sign(anotherjson.stringify(k)); diff --git a/src/crypto/deviceinfo.ts b/src/crypto/deviceinfo.ts index 3b4d53f6813..a8fd9f0086b 100644 --- a/src/crypto/deviceinfo.ts +++ b/src/crypto/deviceinfo.ts @@ -72,7 +72,8 @@ export class DeviceInfo { const res = new DeviceInfo(deviceId); for (const prop in obj) { if (obj.hasOwnProperty(prop)) { - res[prop] = obj[prop]; + // @ts-ignore - this is messy and typescript doesn't like it + res[prop as keyof IDevice] = obj[prop as keyof IDevice]; } } return res; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index d71fadf9073..9163ace4f71 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -88,6 +88,9 @@ import { CryptoStore } from "./store/base"; import { IVerificationChannel } from "./verification/request/Channel"; import { TypedEventEmitter } from "../models/typed-event-emitter"; import { IContent } from "../models/event"; +import { ISyncResponse } from "../sync-accumulator"; +import { ISignatures } from "../@types/signed"; +import { IMessage } from "./algorithms/olm"; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -100,7 +103,7 @@ const defaultVerificationMethods = { // to start. [SHOW_QR_CODE_METHOD]: IllegalMethod, [SCAN_QR_CODE_METHOD]: IllegalMethod, -}; +} as const; /** * verification method names @@ -109,7 +112,7 @@ const defaultVerificationMethods = { export const verificationMethods = { RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME, SAS: SASVerification.NAME, -}; +} as const; export type VerificationMethod = keyof typeof verificationMethods | string; @@ -200,18 +203,13 @@ interface IUserOlmSession { }[]; } -interface ISyncDeviceLists { - changed: string[]; - left: string[]; -} - export interface IRoomKeyRequestRecipient { userId: string; deviceId: string; } interface ISignableObject { - signatures?: object; + signatures?: ISignatures; unsigned?: object; } @@ -231,14 +229,25 @@ export interface IRequestsMap { } /* eslint-disable camelcase */ -export interface IEncryptedContent { - algorithm: string; +export interface IOlmEncryptedContent { + algorithm: typeof olmlib.OLM_ALGORITHM; sender_key: string; - ciphertext: Record; - [ToDeviceMessageId]: string; + ciphertext: Record; + [ToDeviceMessageId]?: string; +} + +export interface IMegolmEncryptedContent { + algorithm: typeof olmlib.MEGOLM_ALGORITHM; + sender_key: string; + session_id: string; + device_id: string; + ciphertext: string; + [ToDeviceMessageId]?: string; } /* eslint-enable camelcase */ +export type IEncryptedContent = IOlmEncryptedContent | IMegolmEncryptedContent; + export enum CryptoEvent { DeviceVerificationChanged = "deviceVerificationChanged", UserTrustStatusChanged = "userTrustStatusChanged", @@ -382,7 +391,7 @@ export class Crypto extends TypedEventEmitter, + verificationMethods: Array, ) { super(); this.reEmitter = new TypedReEmitter(this); @@ -2947,7 +2956,10 @@ export class Crypto extends TypedEventEmitter { + public async handleDeviceListChanges( + syncData: ISyncStateData, + syncDeviceLists: Required["device_lists"], + ): Promise { // Initial syncs don't have device change lists. We'll either get the complete list // of changes for the interval or will have invalidated everything in willProcessSync if (!syncData.oldSyncToken) return; @@ -3087,15 +3099,14 @@ export class Crypto extends TypedEventEmitter { - if (deviceLists.changed && Array.isArray(deviceLists.changed)) { + private async evalDeviceListChanges(deviceLists: Required["device_lists"]): Promise { + if (Array.isArray(deviceLists?.changed)) { deviceLists.changed.forEach((u) => { this.deviceList.invalidateUserDeviceList(u); }); } - if (deviceLists.left && Array.isArray(deviceLists.left) && - deviceLists.left.length) { + if (Array.isArray(deviceLists?.left) && deviceLists.left.length) { // Check we really don't share any rooms with these users // any more: the server isn't required to give us the // exact correct set. @@ -3515,9 +3526,9 @@ export class Crypto extends TypedEventEmitter { + public async signObject(obj: T): Promise { const sigs = obj.signatures || {}; const unsigned = obj.unsigned; diff --git a/src/crypto/olmlib.ts b/src/crypto/olmlib.ts index 111e4c16de3..862691353c9 100644 --- a/src/crypto/olmlib.ts +++ b/src/crypto/olmlib.ts @@ -31,6 +31,7 @@ import { IClaimOTKsResult, MatrixClient } from "../client"; import { ISignatures } from "../@types/signed"; import { MatrixEvent } from "../models/event"; import { EventType } from "../@types/event"; +import { IMessage } from "./algorithms/olm"; enum Algorithm { Olm = "m.olm.v1.curve25519-aes-sha2", @@ -75,7 +76,7 @@ export interface IOlmSessionResult { * has been encrypted into `resultsObject` */ export async function encryptMessageForDevice( - resultsObject: Record, + resultsObject: Record, ourUserId: string, ourDeviceId: string | undefined, olmDevice: OlmDevice, @@ -124,6 +125,7 @@ export async function encryptMessageForDevice( recipient_keys: { "ed25519": recipientDevice.getFingerprint(), }, + ...payloadFields, }; // TODO: technically, a bunch of that stuff only needs to be included for @@ -131,11 +133,7 @@ export async function encryptMessageForDevice( // involved in the session. If we're looking to reduce data transfer in the // future, we could elide them for subsequent messages. - Object.assign(payload, payloadFields); - - resultsObject[deviceKey] = await olmDevice.encryptMessage( - deviceKey, sessionId, JSON.stringify(payload), - ); + resultsObject[deviceKey] = await olmDevice.encryptMessage(deviceKey, sessionId, JSON.stringify(payload)); } interface IExistingOlmSession { @@ -495,7 +493,7 @@ export async function verifySignature( * @param {string} pubKey The public key (ignored if key is a seed) * @returns {string} the signature for the object */ -export function pkSign(obj: IObject, key: PkSigning, userId: string, pubKey: string): string { +export function pkSign(obj: object & IObject, key: Uint8Array | PkSigning, userId: string, pubKey: string): string { let createdKey = false; if (key instanceof Uint8Array) { const keyObj = new global.Olm.PkSigning(); diff --git a/src/crypto/store/base.ts b/src/crypto/store/base.ts index 0615822271d..72b02e0a1cb 100644 --- a/src/crypto/store/base.ts +++ b/src/crypto/store/base.ts @@ -69,7 +69,7 @@ export interface CryptoStore { deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise; // Olm Account - getAccount(txn: unknown, func: (accountPickle: string | null) => void); + getAccount(txn: unknown, func: (accountPickle: string | null) => void): void; storeAccount(txn: unknown, accountPickle: string): void; getCrossSigningKeys(txn: unknown, func: (keys: Record | null) => void): void; getSecretStorePrivateKey( diff --git a/src/crypto/store/localStorage-crypto-store.ts b/src/crypto/store/localStorage-crypto-store.ts index 977236ef93f..c6f9a02d6ae 100644 --- a/src/crypto/store/localStorage-crypto-store.ts +++ b/src/crypto/store/localStorage-crypto-store.ts @@ -338,20 +338,20 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { } public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise { - const sessionsNeedingBackup - = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + const sessionsNeedingBackup = getJsonItem<{ + [senderKeySessionId: string]: string; + }>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; for (const session of sessions) { delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId]; } - setJsonItem( - this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup, - ); + setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup); return Promise.resolve(); } public markSessionsNeedingBackup(sessions: ISession[]): Promise { - const sessionsNeedingBackup - = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + const sessionsNeedingBackup = getJsonItem<{ + [senderKeySessionId: string]: boolean; + }>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; for (const session of sessions) { sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; } diff --git a/src/crypto/verification/QRCode.ts b/src/crypto/verification/QRCode.ts index f6bdda17e1a..13053280d8b 100644 --- a/src/crypto/verification/QRCode.ts +++ b/src/crypto/verification/QRCode.ts @@ -283,21 +283,21 @@ export class QRCodeData { private static generateBuffer(qrData: IQrData): Buffer { let buf = Buffer.alloc(0); // we'll concat our way through life - const appendByte = (b): void => { + const appendByte = (b: number): void => { const tmpBuf = Buffer.from([b]); buf = Buffer.concat([buf, tmpBuf]); }; - const appendInt = (i): void => { + const appendInt = (i: number): void => { const tmpBuf = Buffer.alloc(2); tmpBuf.writeInt16BE(i, 0); buf = Buffer.concat([buf, tmpBuf]); }; - const appendStr = (s, enc, withLengthPrefix = true): void => { + const appendStr = (s: string, enc: BufferEncoding, withLengthPrefix = true): void => { const tmpBuf = Buffer.from(s, enc); if (withLengthPrefix) appendInt(tmpBuf.byteLength); buf = Buffer.concat([buf, tmpBuf]); }; - const appendEncBase64 = (b64): void => { + const appendEncBase64 = (b64: string): void => { const b = decodeBase64(b64); const tmpBuf = Buffer.from(b); buf = Buffer.concat([buf, tmpBuf]); @@ -307,7 +307,7 @@ export class QRCodeData { appendStr(qrData.prefix, "ascii", false); appendByte(qrData.version); appendByte(qrData.mode); - appendStr(qrData.transactionId, "utf-8"); + appendStr(qrData.transactionId!, "utf-8"); appendEncBase64(qrData.firstKeyB64); appendEncBase64(qrData.secondKeyB64); appendEncBase64(qrData.secretB64); diff --git a/src/crypto/verification/SAS.ts b/src/crypto/verification/SAS.ts index 5df6d48f6c8..168a85d28fc 100644 --- a/src/crypto/verification/SAS.ts +++ b/src/crypto/verification/SAS.ts @@ -140,7 +140,7 @@ function generateEmojiSas(sasBytes: number[]): EmojiMapping[] { const sasGenerators = { decimal: generateDecimalSas, emoji: generateEmojiSas, -}; +} as const; export interface IGeneratedSas { decimal?: [number, number, number]; @@ -154,11 +154,12 @@ export interface ISasEvent { mismatch(): void; } -function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas { +function generateSas(sasBytes: Uint8Array, methods: string[]): IGeneratedSas { const sas: IGeneratedSas = {}; for (const method of methods) { if (method in sasGenerators) { - sas[method] = sasGenerators[method](sasBytes); + // @ts-ignore - ts doesn't like us mixing types like this + sas[method] = sasGenerators[method](Array.from(sasBytes)); } } return sas; @@ -168,15 +169,14 @@ const macMethods = { "hkdf-hmac-sha256": "calculate_mac", "org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64", "hmac-sha256": "calculate_mac_long_kdf", -}; +} as const; -type Method = keyof typeof macMethods; +type MacMethod = keyof typeof macMethods; -function calculateMAC(olmSAS: OlmSAS, method: Method) { - return function(...args): string { - const macFunction = olmSAS[macMethods[method]]; - const mac: string = macFunction.apply(olmSAS, args); - logger.log("SAS calculateMAC:", method, args, mac); +function calculateMAC(olmSAS: OlmSAS, method: MacMethod) { + return function(input: string, info: string): string { + const mac = olmSAS[macMethods[method]](input, info); + logger.log("SAS calculateMAC:", method, [input, info], mac); return mac; }; } @@ -202,15 +202,17 @@ const calculateKeyAgreement = { + sas.channel.transactionId; return olmSAS.generate_bytes(sasInfo, bytes); }, -}; +} as const; + +type KeyAgreement = keyof typeof calculateKeyAgreement; /* lists of algorithms/methods that are supported. The key agreement, hashes, * and MAC lists should be sorted in order of preference (most preferred * first). */ -const KEY_AGREEMENT_LIST = ["curve25519-hkdf-sha256", "curve25519"]; +const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"]; const HASHES_LIST = ["sha256"]; -const MAC_LIST: Method[] = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"]; +const MAC_LIST: MacMethod[] = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"]; const SAS_LIST = Object.keys(sasGenerators); const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST); @@ -299,10 +301,10 @@ export class SAS extends Base { } private async verifyAndCheckMAC( - keyAgreement: string, + keyAgreement: KeyAgreement, sasMethods: string[], olmSAS: OlmSAS, - macMethod: Method, + macMethod: MacMethod, ): Promise { const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6); const verifySAS = new Promise((resolve, reject) => { @@ -354,7 +356,7 @@ export class SAS extends Base { throw new SwitchStartEventError(this.startEvent); } - let e; + let e: MatrixEvent; try { e = await this.waitForEvent(EventType.KeyVerificationAccept); } finally { @@ -445,8 +447,8 @@ export class SAS extends Base { } } - private sendMAC(olmSAS: OlmSAS, method: Method): Promise { - const mac = {}; + private sendMAC(olmSAS: OlmSAS, method: MacMethod): Promise { + const mac: Record = {}; const keyList: string[] = []; const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this.baseApis.getUserId() + this.baseApis.deviceId @@ -455,7 +457,7 @@ export class SAS extends Base { const deviceKeyId = `ed25519:${this.baseApis.deviceId}`; mac[deviceKeyId] = calculateMAC(olmSAS, method)( - this.baseApis.getDeviceEd25519Key(), + this.baseApis.getDeviceEd25519Key()!, baseInfo + deviceKeyId, ); keyList.push(deviceKeyId); @@ -477,7 +479,7 @@ export class SAS extends Base { return this.send(EventType.KeyVerificationMac, { mac, keys }); } - private async checkMAC(olmSAS: OlmSAS, content: IContent, method: Method): Promise { + private async checkMAC(olmSAS: OlmSAS, content: IContent, method: MacMethod): Promise { const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this.userId + this.deviceId + this.baseApis.getUserId() + this.baseApis.deviceId diff --git a/src/filter-component.ts b/src/filter-component.ts index 85afb0ea707..0bda7d3792d 100644 --- a/src/filter-component.ts +++ b/src/filter-component.ts @@ -148,17 +148,17 @@ export class FilterComponent { "types": function(v: string): boolean { return matchesWildcard(eventType, v); }, - }; + } as const; for (const name in literalKeys) { - const matchFunc = literalKeys[name]; + const matchFunc = literalKeys[name]; const notName = "not_" + name; - const disallowedValues: string[] = this.filterJson[notName]; + const disallowedValues = this.filterJson[<`not_${keyof typeof literalKeys}`>notName]; if (disallowedValues?.some(matchFunc)) { return false; } - const allowedValues: string[] = this.filterJson[name]; + const allowedValues = this.filterJson[name as keyof typeof literalKeys]; if (allowedValues && !allowedValues.some(matchFunc)) { return false; } diff --git a/src/filter.ts b/src/filter.ts index 57bd0540d87..66522701730 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -31,8 +31,8 @@ import { MatrixEvent } from "./models/event"; * @param {string} keyNesting * @param {*} val */ -function setProp(obj: object, keyNesting: string, val: any): void { - const nestedKeys = keyNesting.split("."); +function setProp(obj: Record, keyNesting: string, val: any): void { + const nestedKeys = keyNesting.split(".") as [keyof typeof obj]; let currentObj = obj; for (let i = 0; i < (nestedKeys.length - 1); i++) { if (!currentObj[nestedKeys[i]]) { @@ -70,8 +70,7 @@ interface IStateFilter extends IRoomEventFilter {} interface IRoomFilter { not_rooms?: string[]; - rooms?: string[]; - ephemeral?: IRoomEventFilter; + rooms?: string[];ephemeral?: IRoomEventFilter; include_leave?: boolean; state?: IStateFilter; timeline?: IRoomEventFilter; diff --git a/src/models/event.ts b/src/models/event.ts index c233307d4eb..2864aa6b520 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -318,19 +318,19 @@ export class MatrixEvent extends TypedEventEmitter { + (["state_key", "type", "sender", "room_id", "membership"] as const).forEach((prop) => { if (typeof event[prop] !== "string") return; - event[prop] = internaliseString(event[prop]); + event[prop] = internaliseString(event[prop]!); }); - ["membership", "avatar_url", "displayname"].forEach((prop) => { + (["membership", "avatar_url", "displayname"] as const).forEach((prop) => { if (typeof event.content?.[prop] !== "string") return; - event.content[prop] = internaliseString(event.content[prop]); + event.content[prop] = internaliseString(event.content[prop]!); }); - ["rel_type"].forEach((prop) => { + (["rel_type"] as const).forEach((prop) => { if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return; - event.content["m.relates_to"][prop] = internaliseString(event.content["m.relates_to"][prop]); + event.content["m.relates_to"][prop] = internaliseString(event.content["m.relates_to"][prop]!); }); this.txnId = event.txn_id; @@ -1120,7 +1120,7 @@ export class MatrixEvent extends TypedEventEmitter> = { [EventType.RoomMember]: { 'membership': 1 }, [EventType.RoomCreate]: { 'creator': 1 }, [EventType.RoomJoinRules]: { 'join_rule': 1 }, @@ -1621,7 +1624,7 @@ const REDACT_KEEP_CONTENT_MAP = { 'kick': 1, 'redact': 1, 'state_default': 1, 'users': 1, 'users_default': 1, }, -}; +} as const; /** * Fires when an event is decrypted diff --git a/src/models/invites-ignorer.ts b/src/models/invites-ignorer.ts index ad48fe5633f..907d1ca176c 100644 --- a/src/models/invites-ignorer.ts +++ b/src/models/invites-ignorer.ts @@ -17,7 +17,7 @@ limitations under the License. import { UnstableValue } from "matrix-events-sdk"; import { MatrixClient } from "../client"; -import { MatrixEvent } from "./event"; +import { IContent, MatrixEvent } from "./event"; import { EventTimeline } from "./event-timeline"; import { Preset } from "../@types/partials"; import { globToRegexp } from "../utils"; @@ -262,7 +262,7 @@ export class IgnoredInvites { */ public async getOrCreateSourceRooms(): Promise { const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies(); - let sources = ignoreInvitesPolicies.sources; + let sources: string[] = ignoreInvitesPolicies.sources; // Validate `sources`. If it is invalid, trash out the current `sources` // and create a new list of sources from `target`. @@ -272,11 +272,11 @@ export class IgnoredInvites { hasChanges = true; sources = []; } - let sourceRooms: Room[] = sources + let sourceRooms = sources // `sources` could contain non-string / invalid room ids .filter(roomId => typeof roomId === "string") .map(roomId => this.client.getRoom(roomId)) - .filter(room => !!room); + .filter(room => !!room) as Room[]; if (sourceRooms.length != sources.length) { hasChanges = true; } @@ -327,7 +327,7 @@ export class IgnoredInvites { */ private getPoliciesAndIgnoreInvitesPolicies(): {policies: {[key: string]: any}, ignoreInvitesPolicies: {[key: string]: any}} { - let policies = {}; + let policies: IContent = {}; for (const key of [POLICIES_ACCOUNT_EVENT_TYPE.name, POLICIES_ACCOUNT_EVENT_TYPE.altName]) { if (!key) { continue; diff --git a/src/models/room-state.ts b/src/models/room-state.ts index 523201d8323..e69d9515268 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -22,7 +22,7 @@ import { RoomMember } from "./room-member"; import { logger } from '../logger'; import * as utils from "../utils"; import { EventType, UNSTABLE_MSC2716_MARKER } from "../@types/event"; -import { MatrixEvent, MatrixEventEvent } from "./event"; +import { IEvent, MatrixEvent, MatrixEventEvent } from "./event"; import { MatrixClient } from "../client"; import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials"; import { TypedEventEmitter } from "./typed-event-emitter"; @@ -53,6 +53,20 @@ enum OobStatus { Finished, } +export interface IPowerLevelsContent { + users?: Record; + events?: Record; + // eslint-disable-next-line camelcase + users_default?: number; + // eslint-disable-next-line camelcase + events_default?: number; + // eslint-disable-next-line camelcase + state_default?: number; + ban?: number; + kick?: number; + redact?: number; +} + export enum RoomStateEvent { Events = "RoomState.events", Members = "RoomState.members", @@ -499,7 +513,7 @@ export class RoomState extends TypedEventEmitter const beacon = this.beacons.get(beaconIdentifier)!; if (event.isRedacted()) { - if (beacon.beaconInfoId === event.getRedactionEvent()?.['redacts']) { + if (beacon.beaconInfoId === (event.getRedactionEvent())?.redacts) { beacon.destroy(); this.beacons.delete(beaconIdentifier); } @@ -724,17 +738,17 @@ export class RoomState extends TypedEventEmitter * @param {number} powerLevel The power level of the member * @return {boolean} true if the given power level is sufficient */ - public hasSufficientPowerLevelFor(action: string, powerLevel: number): boolean { + public hasSufficientPowerLevelFor(action: "ban" | "kick" | "redact", powerLevel: number): boolean { const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, ""); - let powerLevels = {}; + let powerLevels: IPowerLevelsContent = {}; if (powerLevelsEvent) { powerLevels = powerLevelsEvent.getContent(); } let requiredLevel = 50; if (utils.isNumber(powerLevels[action])) { - requiredLevel = powerLevels[action]; + requiredLevel = powerLevels[action]!; } return powerLevel >= requiredLevel; @@ -807,8 +821,8 @@ export class RoomState extends TypedEventEmitter private maySendEventOfType(eventType: EventType | string, userId: string, state: boolean): boolean { const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, ''); - let powerLevels; - let eventsLevels = {}; + let powerLevels: IPowerLevelsContent; + let eventsLevels: Record = {}; let stateDefault = 0; let eventsDefault = 0; @@ -818,20 +832,20 @@ export class RoomState extends TypedEventEmitter eventsLevels = powerLevels.events || {}; if (Number.isSafeInteger(powerLevels.state_default)) { - stateDefault = powerLevels.state_default; + stateDefault = powerLevels.state_default!; } else { stateDefault = 50; } const userPowerLevel = powerLevels.users && powerLevels.users[userId]; if (Number.isSafeInteger(userPowerLevel)) { - powerLevel = userPowerLevel; + powerLevel = userPowerLevel!; } else if (Number.isSafeInteger(powerLevels.users_default)) { - powerLevel = powerLevels.users_default; + powerLevel = powerLevels.users_default!; } if (Number.isSafeInteger(powerLevels.events_default)) { - eventsDefault = powerLevels.events_default; + eventsDefault = powerLevels.events_default!; } } @@ -877,7 +891,7 @@ export class RoomState extends TypedEventEmitter */ public getJoinRule(): JoinRule { const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, ""); - const joinRuleContent = joinRuleEvent?.getContent() ?? {}; + const joinRuleContent: Partial = joinRuleEvent?.getContent() ?? {}; return joinRuleContent["join_rule"] || JoinRule.Invite; } diff --git a/src/models/room-summary.ts b/src/models/room-summary.ts index ba279d2d4fc..218a4afcc8d 100644 --- a/src/models/room-summary.ts +++ b/src/models/room-summary.ts @@ -20,8 +20,8 @@ limitations under the License. export interface IRoomSummary { "m.heroes": string[]; - "m.joined_member_count": number; - "m.invited_member_count": number; + "m.joined_member_count"?: number; + "m.invited_member_count"?: number; } interface IInfo { diff --git a/src/models/room.ts b/src/models/room.ts index 3952ed7d9cb..4c3ebafbe82 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -1312,10 +1312,10 @@ export class Room extends ReadReceipt { const joinedCount = summary["m.joined_member_count"]; const invitedCount = summary["m.invited_member_count"]; if (Number.isInteger(joinedCount)) { - this.currentState.setJoinedMemberCount(joinedCount); + this.currentState.setJoinedMemberCount(joinedCount!); } if (Number.isInteger(invitedCount)) { - this.currentState.setInvitedMemberCount(invitedCount); + this.currentState.setInvitedMemberCount(invitedCount!); } if (Array.isArray(heroes)) { // be cautious about trusting server values, @@ -1826,7 +1826,7 @@ export class Room extends ReadReceipt { timelineSet.getFilter(), ); - timelineSet.getLiveTimeline().setPaginationToken(end, Direction.Backward); + timelineSet.getLiveTimeline().setPaginationToken(end ?? null, Direction.Backward); if (!events.length) return; diff --git a/src/pushprocessor.ts b/src/pushprocessor.ts index 4538b97214e..cc124682591 100644 --- a/src/pushprocessor.ts +++ b/src/pushprocessor.ts @@ -518,13 +518,13 @@ export class PushProcessor { * @return {object} The push rule, or null if no such rule was found */ public getPushRuleById(ruleId: string): IPushRule | null { - for (const scope of ['global']) { + for (const scope of ['global'] as const) { if (this.client.pushRules?.[scope] === undefined) continue; for (const kind of RULEKINDS_IN_ORDER) { if (this.client.pushRules[scope][kind] === undefined) continue; - for (const rule of this.client.pushRules[scope][kind]) { + for (const rule of this.client.pushRules[scope][kind]!) { if (rule.rule_id === ruleId) return rule; } } diff --git a/src/scheduler.ts b/src/scheduler.ts index 0c15032bcac..74bf7dd2580 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -97,7 +97,7 @@ export class MatrixScheduler { * @see module:scheduler~queueAlgorithm */ // eslint-disable-next-line @typescript-eslint/naming-convention - public static QUEUE_MESSAGES(event: MatrixEvent): "message" | null { + public static QUEUE_MESSAGES(event: MatrixEvent): string | null { // enqueue messages or events that associate with another event (redactions and relations) if (event.getType() === EventType.RoomMessage || event.hasAssociation()) { // put these events in the 'message' queue. diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index c99f61f13b9..d103eb39bea 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -22,7 +22,7 @@ import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } fr import { ISyncStateData, SyncState, _createAndReEmitRoom } from "./sync"; import { MatrixEvent } from "./models/event"; import { Crypto } from "./crypto"; -import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState } from "./sync-accumulator"; +import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState, ISyncResponse } from "./sync-accumulator"; import { MatrixError } from "./http-api"; import { Extension, @@ -44,7 +44,18 @@ import { RoomMemberEvent } from "./models/room-member"; // keepAlive is successful but the server /sync fails. const FAILED_SYNC_ERROR_THRESHOLD = 3; -class ExtensionE2EE implements Extension { +type ExtensionE2EERequest = { + enabled: boolean; +}; + +type ExtensionE2EEResponse = Pick; + +class ExtensionE2EE implements Extension { public constructor(private readonly crypto: Crypto) {} public name(): string { @@ -55,7 +66,7 @@ class ExtensionE2EE implements Extension { return ExtensionState.PreProcess; } - public onRequest(isInitial: boolean): object | undefined { + public onRequest(isInitial: boolean): ExtensionE2EERequest | undefined { if (!isInitial) { return undefined; } @@ -64,7 +75,7 @@ class ExtensionE2EE implements Extension { }; } - public async onResponse(data: object): Promise { + public async onResponse(data: ExtensionE2EEResponse): Promise { // Handle device list updates if (data["device_lists"]) { await this.crypto.handleDeviceListChanges({ @@ -92,7 +103,18 @@ class ExtensionE2EE implements Extension { } } -class ExtensionToDevice implements Extension { +type ExtensionToDeviceRequest = { + since?: string; + limit?: number; + enabled?: boolean; +}; + +type ExtensionToDeviceResponse = { + events: Required["to_device"]["events"]; + next_batch: string | null; +}; + +class ExtensionToDevice implements Extension { private nextBatch: string | null = null; public constructor(private readonly client: MatrixClient) {} @@ -105,8 +127,8 @@ class ExtensionToDevice implements Extension { return ExtensionState.PreProcess; } - public onRequest(isInitial: boolean): object { - const extReq = { + public onRequest(isInitial: boolean): ExtensionToDeviceRequest { + const extReq: ExtensionToDeviceRequest = { since: this.nextBatch !== null ? this.nextBatch : undefined, }; if (isInitial) { @@ -116,11 +138,10 @@ class ExtensionToDevice implements Extension { return extReq; } - public async onResponse(data: object): Promise { + public async onResponse(data: ExtensionToDeviceResponse): Promise { const cancelledKeyVerificationTxns: string[] = []; - data["events"] = data["events"] || []; - data["events"] - .map(this.client.getEventMapper()) + data.events + ?.map(this.client.getEventMapper()) .map((toDeviceEvent) => { // map is a cheap inline forEach // We want to flag m.key.verification.start events as cancelled // if there's an accompanying m.key.verification.cancel event, so @@ -165,11 +186,20 @@ class ExtensionToDevice implements Extension { }, ); - this.nextBatch = data["next_batch"]; + this.nextBatch = data.next_batch; } } -class ExtensionAccountData implements Extension { +type ExtensionAccountDataRequest = { + enabled: boolean; +}; + +type ExtensionAccountDataResponse = { + global: IMinimalEvent[]; + rooms: Record; +}; + +class ExtensionAccountData implements Extension { public constructor(private readonly client: MatrixClient) {} public name(): string { @@ -180,7 +210,7 @@ class ExtensionAccountData implements Extension { return ExtensionState.PostProcess; } - public onRequest(isInitial: boolean): object | undefined { + public onRequest(isInitial: boolean): ExtensionAccountDataRequest | undefined { if (!isInitial) { return undefined; } @@ -189,7 +219,7 @@ class ExtensionAccountData implements Extension { }; } - public onResponse(data: {global: object[], rooms: Record}): void { + public onResponse(data: ExtensionAccountDataResponse): void { if (data.global && data.global.length > 0) { this.processGlobalAccountData(data.global); } @@ -208,9 +238,9 @@ class ExtensionAccountData implements Extension { } } - private processGlobalAccountData(globalAccountData: object[]): void { + private processGlobalAccountData(globalAccountData: IMinimalEvent[]): void { const events = mapEvents(this.client, undefined, globalAccountData); - const prevEventsMap = events.reduce((m, c) => { + const prevEventsMap = events.reduce>((m, c) => { m[c.getType()] = this.client.store.getAccountData(c.getType()); return m; }, {}); @@ -233,7 +263,15 @@ class ExtensionAccountData implements Extension { } } -class ExtensionTyping implements Extension { +type ExtensionTypingRequest = { + enabled: boolean; +}; + +type ExtensionTypingResponse = { + rooms: Record; +}; + +class ExtensionTyping implements Extension { public constructor(private readonly client: MatrixClient) {} public name(): string { @@ -244,7 +282,7 @@ class ExtensionTyping implements Extension { return ExtensionState.PostProcess; } - public onRequest(isInitial: boolean): object | undefined { + public onRequest(isInitial: boolean): ExtensionTypingRequest | undefined { if (!isInitial) { return undefined; // don't send a JSON object for subsequent requests, we don't need to. } @@ -253,20 +291,26 @@ class ExtensionTyping implements Extension { }; } - public onResponse(data: {rooms: Record}): void { - if (!data || !data.rooms) { + public onResponse(data: ExtensionTypingResponse): void { + if (!data?.rooms) { return; } for (const roomId in data.rooms) { - processEphemeralEvents( - this.client, roomId, [data.rooms[roomId]], - ); + processEphemeralEvents(this.client, roomId, [data.rooms[roomId]]); } } } -class ExtensionReceipts implements Extension { +type ExtensionReceiptsRequest = { + enabled: boolean; +}; + +type ExtensionReceiptsResponse = { + rooms: Record; +}; + +class ExtensionReceipts implements Extension { public constructor(private readonly client: MatrixClient) {} public name(): string { @@ -277,7 +321,7 @@ class ExtensionReceipts implements Extension { return ExtensionState.PostProcess; } - public onRequest(isInitial: boolean): object | undefined { + public onRequest(isInitial: boolean): ExtensionReceiptsRequest | undefined { if (isInitial) { return { enabled: true, @@ -286,8 +330,8 @@ class ExtensionReceipts implements Extension { return undefined; // don't send a JSON object for subsequent requests, we don't need to. } - public onResponse(data: {rooms: Record}): void { - if (!data || !data.rooms) { + public onResponse(data: ExtensionReceiptsResponse): void { + if (!data?.rooms) { return; } @@ -336,7 +380,7 @@ export class SlidingSyncSdk { this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this)); this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this)); - const extensions: Extension[] = [ + const extensions: Extension[] = [ new ExtensionToDevice(this.client), new ExtensionAccountData(this.client), new ExtensionTyping(this.client), @@ -533,7 +577,7 @@ export class SlidingSyncSdk { // room::decryptCriticalEvent is in charge of decrypting all the events // required for a client to function properly let timelineEvents = mapEvents(this.client, room.roomId, roomData.timeline, false); - const ephemeralEvents = []; // TODO this.mapSyncEventsFormat(joinObj.ephemeral); + const ephemeralEvents: MatrixEvent[] = []; // TODO this.mapSyncEventsFormat(joinObj.ephemeral); // TODO: handle threaded / beacon events @@ -967,12 +1011,14 @@ function ensureNameEvent(client: MatrixClient, roomId: string, roomData: MSC3575 return roomData; } +type TaggedEvent = (IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent) & { room_id?: string }; + // Helper functions which set up JS SDK structs are below and are identical to the sync v2 counterparts, // just outside the class. function mapEvents(client: MatrixClient, roomId: string | undefined, events: object[], decrypt = true): MatrixEvent[] { const mapper = client.getEventMapper({ decrypt }); - return (events as Array).map(function(e) { - e["room_id"] = roomId; + return (events as TaggedEvent[]).map(function(e) { + e.room_id = roomId; return mapper(e); }); } diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index cda2737d984..a3daf3af6bf 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -139,7 +139,7 @@ export interface MSC3575SlidingSyncResponse { txn_id?: string; lists: ListResponse[]; rooms: Record; - extensions: object; + extensions: Record; } export enum SlidingSyncState { @@ -265,7 +265,7 @@ export enum ExtensionState { /** * An interface that must be satisfied to register extensions */ -export interface Extension { +export interface Extension { /** * The extension name to go under 'extensions' in the request body. * @returns The JSON key. @@ -277,12 +277,12 @@ export interface Extension { * @param isInitial True when this is part of the initial request (send sticky params) * @returns The request JSON to send. */ - onRequest(isInitial: boolean): object | undefined; + onRequest(isInitial: boolean): Req | undefined; /** * A function which is called when there is response JSON under this extension. * @param data The response JSON under the extension name. */ - onResponse(data: object); + onResponse(data: Res): void; /** * Controls when onResponse should be called. * @returns The state when it should be called. @@ -353,7 +353,7 @@ export class SlidingSync extends TypedEventEmitter & { txnId: string})[] = []; // map of extension name to req/resp handler - private extensions: Record = {}; + private extensions: Record> = {}; private desiredRoomSubscriptions = new Set(); // the *desired* room subscriptions private confirmedRoomSubscriptions = new Set(); @@ -519,22 +519,22 @@ export class SlidingSync extends TypedEventEmitter): void { if (this.extensions[ext.name()]) { throw new Error(`registerExtension: ${ext.name()} already exists as an extension`); } this.extensions[ext.name()] = ext; } - private getExtensionRequest(isInitial: boolean): object { - const ext = {}; + private getExtensionRequest(isInitial: boolean): Record { + const ext: Record = {}; Object.keys(this.extensions).forEach((extName) => { ext[extName] = this.extensions[extName].onRequest(isInitial); }); return ext; } - private onPreExtensionsResponse(ext: object): void { + private onPreExtensionsResponse(ext: Record): void { Object.keys(ext).forEach((extName) => { if (this.extensions[extName].when() == ExtensionState.PreProcess) { this.extensions[extName].onResponse(ext[extName]); @@ -542,7 +542,7 @@ export class SlidingSync extends TypedEventEmitter): void { Object.keys(ext).forEach((extName) => { if (this.extensions[extName].when() == ExtensionState.PostProcess) { this.extensions[extName].onResponse(ext[extName]); diff --git a/src/store/index.ts b/src/store/index.ts index bed7aa7a69c..3edba926977 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -123,7 +123,7 @@ export interface IStore { * @param {string} token The token associated with these events. * @param {boolean} toStart True if these are paginated results. */ - storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void; + storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void; /** * Store a filter. diff --git a/src/store/indexeddb-local-backend.ts b/src/store/indexeddb-local-backend.ts index af4b5fc6519..2b94af25600 100644 --- a/src/store/indexeddb-local-backend.ts +++ b/src/store/indexeddb-local-backend.ts @@ -315,10 +315,10 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { const minStateKeyProm = reqAsCursorPromise( roomIndex.openKeyCursor(roomRange, "next"), - ).then((cursor) => cursor && cursor.primaryKey[1]); + ).then((cursor) => (cursor?.primaryKey)[1]); const maxStateKeyProm = reqAsCursorPromise( roomIndex.openKeyCursor(roomRange, "prev"), - ).then((cursor) => cursor && cursor.primaryKey[1]); + ).then((cursor) => (cursor?.primaryKey)[1]); const [minStateKey, maxStateKey] = await Promise.all( [minStateKeyProm, maxStateKeyProm]); diff --git a/src/store/memory.ts b/src/store/memory.ts index f24ab2d976d..1fcb6b0c865 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -217,7 +217,7 @@ export class MemoryStore implements IStore { * @param {string} token The token associated with these events. * @param {boolean} toStart True if these are paginated results. */ - public storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void { + public storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void { // no-op because they've already been added to the room instance. } diff --git a/src/store/stub.ts b/src/store/stub.ts index 746bc521ffb..74234393879 100644 --- a/src/store/stub.ts +++ b/src/store/stub.ts @@ -139,7 +139,7 @@ export class StubStore implements IStore { * @param {string} token The token associated with these events. * @param {boolean} toStart True if these are paginated results. */ - public storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void {} + public storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void {} /** * Store a filter. diff --git a/src/sync-accumulator.ts b/src/sync-accumulator.ts index 19d5cf1312e..f383cb98a4a 100644 --- a/src/sync-accumulator.ts +++ b/src/sync-accumulator.ts @@ -116,7 +116,7 @@ interface IAccountData { events: IMinimalEvent[]; } -interface IToDeviceEvent { +export interface IToDeviceEvent { content: IContent; sender: string; type: string; @@ -127,8 +127,8 @@ interface IToDevice { } interface IDeviceLists { - changed: string[]; - left: string[]; + changed?: string[]; + left?: string[]; } export interface ISyncResponse { @@ -139,6 +139,9 @@ export interface ISyncResponse { to_device?: IToDevice; device_lists?: IDeviceLists; device_one_time_keys_count?: Record; + + device_unused_fallback_key_types?: string[]; + "org.matrix.msc2732.device_unused_fallback_key_types"?: string[]; } /* eslint-enable camelcase */ @@ -182,6 +185,12 @@ export interface ISyncData { roomsData: IRooms; } +type TaggedEvent = IRoomEvent & { _localTs?: number }; + +function isTaggedEvent(event: IRoomEvent): event is TaggedEvent { + return "_localTs" in event && event["_localTs"] !== undefined; +} + /** * The purpose of this class is to accumulate /sync responses such that a * complete "initial" JSON response can be returned which accurately represents @@ -473,35 +482,31 @@ export class SyncAccumulator { // - existing state which didn't come down /sync. // - State events under the 'state' key. // - State events in the 'timeline'. - if (data.state && data.state.events) { - data.state.events.forEach((e) => { - setState(currentData._currentState, e); - }); - } - if (data.timeline && data.timeline.events) { - data.timeline.events.forEach((e, index) => { - // this nops if 'e' isn't a state event - setState(currentData._currentState, e); - // append the event to the timeline. The back-pagination token - // corresponds to the first event in the timeline - let transformedEvent: IRoomEvent & { _localTs?: number }; - if (!fromDatabase) { - transformedEvent = Object.assign({}, e); - if (transformedEvent.unsigned !== undefined) { - transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned); - } - const age = e.unsigned ? e.unsigned.age : e.age; - if (age !== undefined) transformedEvent._localTs = Date.now() - age; - } else { - transformedEvent = e; + data.state?.events?.forEach((e) => { + setState(currentData._currentState, e); + }); + data.timeline?.events?.forEach((e, index) => { + // this nops if 'e' isn't a state event + setState(currentData._currentState, e); + // append the event to the timeline. The back-pagination token + // corresponds to the first event in the timeline + let transformedEvent: TaggedEvent; + if (!fromDatabase) { + transformedEvent = Object.assign({}, e); + if (transformedEvent.unsigned !== undefined) { + transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned); } + const age = e.unsigned ? e.unsigned.age : e.age; + if (age !== undefined) transformedEvent._localTs = Date.now() - age; + } else { + transformedEvent = e; + } - currentData._timeline.push({ - event: transformedEvent, - token: index === 0 ? (data.timeline.prev_batch ?? null) : null, - }); + currentData._timeline.push({ + event: transformedEvent, + token: index === 0 ? (data.timeline.prev_batch ?? null) : null, }); - } + }); // attempt to prune the timeline by jumping between events which have // pagination tokens. @@ -581,7 +586,7 @@ export class SyncAccumulator { room_id: roomId, content: { // $event_id: { "m.read": { $user_id: $json } } - }, + } as IContent, }; for (const [userId, receiptData] of Object.entries(roomData._readReceipts)) { @@ -626,8 +631,8 @@ export class SyncAccumulator { } let transformedEvent: (IRoomEvent | IStateEvent) & { _localTs?: number }; - if (!forDatabase && msgData.event["_localTs"]) { - // This means we have to copy each event so we can fix it up to + if (!forDatabase && isTaggedEvent(msgData.event)) { + // This means we have to copy each event, so we can fix it up to // set a correct 'age' parameter whilst keeping the local timestamp // on our stored event. If this turns out to be a bottleneck, it could // be optimised either by doing this in the main process after the data @@ -641,7 +646,7 @@ export class SyncAccumulator { } delete transformedEvent._localTs; transformedEvent.unsigned = transformedEvent.unsigned || {}; - transformedEvent.unsigned.age = Date.now() - msgData.event["_localTs"]; + transformedEvent.unsigned.age = Date.now() - msgData.event._localTs!; } else { transformedEvent = msgData.event; } diff --git a/src/sync.ts b/src/sync.ts index 20cab67b6eb..c97ba35401d 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -106,7 +106,7 @@ function getFilterName(userId: string, suffix?: string): string { return `FILTER_SYNC_${userId}` + (suffix ? "_" + suffix : ""); } -function debuglog(...params): void { +function debuglog(...params: any[]): void { if (!DEBUG) return; logger.log(...params); } @@ -1093,7 +1093,7 @@ export class SyncApi { // handle non-room account_data if (Array.isArray(data.account_data?.events)) { const events = data.account_data.events.map(client.getEventMapper()); - const prevEventsMap = events.reduce((m, c) => { + const prevEventsMap = events.reduce>((m, c) => { m[c.getType()!] = client.store.getAccountData(c.getType()); return m; }, {}); @@ -1473,12 +1473,13 @@ export class SyncApi { this.opts.crypto.updateOneTimeKeyCount(currentCount); } if (this.opts.crypto && - (data["device_unused_fallback_key_types"] || - data["org.matrix.msc2732.device_unused_fallback_key_types"])) { + (data.device_unused_fallback_key_types || + data["org.matrix.msc2732.device_unused_fallback_key_types"]) + ) { // The presence of device_unused_fallback_key_types indicates that the // server supports fallback keys. If there's no unused // signed_curve25519 fallback key we need a new one. - const unusedFallbackKeys = data["device_unused_fallback_key_types"] || + const unusedFallbackKeys = data.device_unused_fallback_key_types || data["org.matrix.msc2732.device_unused_fallback_key_types"]; this.opts.crypto.setNeedsNewFallback( Array.isArray(unusedFallbackKeys) && @@ -1607,9 +1608,10 @@ export class SyncApi { return []; } const mapper = this.client.getEventMapper({ decrypt }); - return (obj.events as Array).map(function(e) { + type TaggedEvent = (IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent) & { room_id?: string }; + return (obj.events as TaggedEvent[]).map(function(e) { if (room) { - e["room_id"] = room.roomId; + e.room_id = room.roomId; } return mapper(e); }); diff --git a/src/utils.ts b/src/utils.ts index 71871d3b737..ef6af6b0668 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -316,7 +316,7 @@ export function deepSortedObjectEntries(obj: any): [string, any][] { * @param {*} value the value to test * @return {boolean} whether or not value is a finite number without type-coercion */ -export function isNumber(value: any): boolean { +export function isNumber(value: any): value is number { return typeof value === 'number' && isFinite(value); } @@ -428,8 +428,8 @@ export interface IDeferred { // Returns a Deferred export function defer(): IDeferred { - let resolve; - let reject; + let resolve!: IDeferred["resolve"]; + let reject!: IDeferred["reject"]; const promise = new Promise((_resolve, _reject) => { resolve = _resolve; @@ -665,18 +665,22 @@ export function compare(a: string, b: string): number { * @param {Object} source * @returns the target object */ -export function recursivelyAssign(target: Object, source: Object, ignoreNullish = false): any { +export function recursivelyAssign>( + target: T1, + source: T2, + ignoreNullish = false, +): T1 & T2 { for (const [sourceKey, sourceValue] of Object.entries(source)) { if (target[sourceKey] instanceof Object && sourceValue) { recursivelyAssign(target[sourceKey], sourceValue); continue; } if ((sourceValue !== null && sourceValue !== undefined) || !ignoreNullish) { - target[sourceKey] = sourceValue; + target[sourceKey as keyof T1] = sourceValue; continue; } } - return target; + return target as T1 & T2; } function getContentTimestampWithFallback(event: MatrixEvent): number { diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 178f75608d7..7d303af4a5c 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -27,7 +27,7 @@ import { parse as parseSdp, write as writeSdp } from "sdp-transform"; import { logger } from '../logger'; import * as utils from '../utils'; -import { MatrixEvent } from '../models/event'; +import { IContent, MatrixEvent } from '../models/event'; import { EventType, ToDeviceMessageId } from '../@types/event'; import { RoomMember } from '../models/room-member'; import { randomString } from '../randomstring'; @@ -1101,7 +1101,7 @@ export class MatrixCall extends TypedEventEmitter Date: Wed, 7 Dec 2022 11:18:32 +0000 Subject: [PATCH 27/60] Update test runner instructions (#2948) --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 19592b0ea8f..bb86a6f9431 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=bugs)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk) -Matrix Javascript SDK +Matrix JavaScript SDK ===================== This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a @@ -370,11 +370,14 @@ To build a browser version from scratch when developing:: $ yarn build ``` -To run tests (Jasmine):: +To run tests (Jest): ``` $ yarn test ``` +> **Note** +> The `sync-browserify.spec.ts` requires a browser build (`yarn build`) in order to pass + To run linting: ``` $ yarn lint From 4a7365f32fe057b29e90e98598797e40e5aac312 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Dec 2022 13:06:41 +0000 Subject: [PATCH 28/60] Fix release documentation (#2949) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2655ed6e7e2..721f45b6253 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: tag="${{ github.ref_name }}" VERSION="${tag#v}" [ ! -e "$VERSION" ] || rm -r $VERSION - cp -r $RUNNER_TEMP/docs/ $VERSION + cp -r $RUNNER_TEMP/_docs/ $VERSION # Add the new directory to the index if it isn't there already if ! grep -q ">Version $VERSION" index.html; then From a9e7a46c566ac81523a04e2cc081d13054f713a9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 7 Dec 2022 13:48:41 +0000 Subject: [PATCH 29/60] Upload device keys during initCrypto (#2872) Rather than waiting for the application to call `.startClient`, upload the device keys during `initCrypto()`. Element-R is going to approach this slightly differently (it wants to manage the decision on key uploads itself), so this lays some groundwork by collecting the libolm-specific bits together. --- spec/integ/matrix-client-crypto.spec.ts | 13 +++++++++---- spec/integ/matrix-client-methods.spec.ts | 14 +++++++++++++- spec/unit/crypto.spec.ts | 8 +++++++- spec/unit/crypto/backup.spec.ts | 11 ++++++++++- src/client.ts | 19 ++++++++----------- 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/spec/integ/matrix-client-crypto.spec.ts b/spec/integ/matrix-client-crypto.spec.ts index adb7dd25b9a..ae58be1c91c 100644 --- a/spec/integ/matrix-client-crypto.spec.ts +++ b/spec/integ/matrix-client-crypto.spec.ts @@ -53,10 +53,7 @@ type OlmPayload = ReturnType; async function bobUploadsDeviceKeys(): Promise { bobTestClient.expectDeviceKeyUpload(); - await Promise.all([ - bobTestClient.client.uploadKeys(), - bobTestClient.httpBackend.flushAllExpected(), - ]); + await bobTestClient.httpBackend.flushAllExpected(); expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0); } @@ -383,6 +380,14 @@ describe("MatrixClient crypto", () => { it("Bob uploads device keys", bobUploadsDeviceKeys); + it("handles failures to upload device keys", async () => { + // since device keys are uploaded asynchronously, there's not really much to do here other than fail the + // upload. + bobTestClient.httpBackend.when("POST", "/keys/upload") + .fail(0, new Error("bleh")); + await bobTestClient.httpBackend.flushAllExpected(); + }); + it("Ali downloads Bobs device keys", async () => { await bobUploadsDeviceKeys(); await aliDownloadsKeys(); diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index 93c3d353727..c585fb3c5fa 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -511,7 +511,12 @@ describe("MatrixClient", function() { } beforeEach(function() { - return client!.initCrypto(); + // running initCrypto should trigger a key upload + httpBackend!.when("POST", "/keys/upload").respond(200, {}); + return Promise.all([ + client!.initCrypto(), + httpBackend!.flush("/keys/upload", 1), + ]); }); afterEach(() => { @@ -1371,6 +1376,13 @@ describe("MatrixClient", function() { await prom; }); }); + + describe("uploadKeys", () => { + // uploadKeys() is a no-op nowadays, so there's not much to test here. + it("should complete successfully", async () => { + await client!.uploadKeys(); + }); + }); }); function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent { diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index a4e1ec374e0..51d57159d52 100644 --- a/spec/unit/crypto.spec.ts +++ b/spec/unit/crypto.spec.ts @@ -995,7 +995,13 @@ describe("Crypto", function() { }); client = new TestClient("@alice:example.org", "aliceweb"); - await client.client.initCrypto(); + + // running initCrypto should trigger a key upload + client.httpBackend.when("POST", "/keys/upload").respond(200, {}); + await Promise.all([ + client.client.initCrypto(), + client.httpBackend.flush("/keys/upload", 1), + ]); encryptedPayload = { algorithm: "m.olm.v1.curve25519-aes-sha2", diff --git a/spec/unit/crypto/backup.spec.ts b/spec/unit/crypto/backup.spec.ts index 60fddedc733..bd1c71f02a3 100644 --- a/spec/unit/crypto/backup.spec.ts +++ b/spec/unit/crypto/backup.spec.ts @@ -135,7 +135,7 @@ function makeTestClient(cryptoStore: CryptoStore) { const scheduler = makeTestScheduler(); const store = new StubStore(); - return new MatrixClient({ + const client = new MatrixClient({ baseUrl: "https://my.home.server", idBaseUrl: "https://identity.server", accessToken: "my.access.token", @@ -147,6 +147,10 @@ function makeTestClient(cryptoStore: CryptoStore) { cryptoStore: cryptoStore, cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys }, }); + + // initialising the crypto library will trigger a key upload request, which we can stub out + client.uploadKeysRequest = jest.fn(); + return client; } describe("MegolmBackup", function() { @@ -509,6 +513,8 @@ describe("MegolmBackup", function() { deviceId: "device", cryptoStore: cryptoStore, }); + // initialising the crypto library will trigger a key upload request, which we can stub out + client.uploadKeysRequest = jest.fn(); megolmDecryption = new MegolmDecryption({ userId: '@user:id', @@ -724,6 +730,9 @@ describe("MegolmBackup", function() { deviceId: "device", cryptoStore, }); + // initialising the crypto library will trigger a key upload request, which we can stub out + client.uploadKeysRequest = jest.fn(); + await client.initCrypto(); cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6); diff --git a/src/client.ts b/src/client.ts index fae73b7a4e4..6fb74ca173c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1210,10 +1210,6 @@ export class MatrixClient extends TypedEventEmitter { @@ -1864,6 +1860,12 @@ export class MatrixClient extends TypedEventEmitter[0]); this.crypto = crypto; + + // upload our keys in the background + this.crypto.uploadDeviceKeys().catch((e) => { + // TODO: throwing away this error is a really bad idea. + logger.error("Error uploading device keys", e); + }); } /** @@ -1895,15 +1897,10 @@ export class MatrixClient extends TypedEventEmitter} A promise that will resolve when the keys are uploaded. + * @deprecated Does nothing. */ public async uploadKeys(): Promise { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - - await this.crypto.uploadDeviceKeys(); + logger.warn("MatrixClient.uploadKeys is deprecated"); } /** From c4006d752a098608ba4f8ea35f5abcd0d78fcddc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Dec 2022 18:01:54 +0000 Subject: [PATCH 30/60] Improve tsdoc types (#2940) * Install eslint-plugin-jsdoc * Enable lint rule jsdoc/no-types * Make tsdoc more valid, add required hyphens and s/return/returns/g * Stash tsdoc work * Fix mistypes * Stash * Stash * More tsdoc work * Remove useless doc params * Fixup docs * Apply suggestions from code review Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Update src/crypto/verification/request/ToDeviceChannel.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Update src/client.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Update src/client.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Update src/client.ts Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Iterate Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- .eslintrc.js | 23 +- package.json | 6 +- spec/TestClient.ts | 10 +- spec/integ/devicelist-integ.spec.ts | 4 +- spec/integ/matrix-client-crypto.spec.ts | 17 +- spec/integ/matrix-client-syncing.spec.ts | 4 +- spec/integ/megolm-integ.spec.ts | 8 +- spec/integ/sliding-sync.spec.ts | 10 +- spec/test-utils/test-utils.ts | 87 +- spec/unit/room.spec.ts | 1 - src/@types/IIdentityServerProvider.ts | 2 +- src/@types/beacon.ts | 65 +- src/@types/event.ts | 6 +- src/@types/requests.ts | 24 + src/@types/topic.ts | 8 +- src/autodiscovery.ts | 34 +- src/client.ts | 2369 +++++++---------- src/content-helpers.ts | 42 +- src/content-repo.ts | 17 +- src/crypto/CrossSigning.ts | 79 +- src/crypto/DeviceList.ts | 79 +- src/crypto/EncryptionSetup.ts | 43 +- src/crypto/OlmDevice.ts | 292 +- src/crypto/OutgoingRoomKeyRequestManager.ts | 27 +- src/crypto/RoomList.ts | 5 - src/crypto/SecretStorage.ts | 45 +- src/crypto/aes.ts | 26 +- src/crypto/algorithms/base.ts | 112 +- src/crypto/algorithms/index.ts | 4 - src/crypto/algorithms/megolm.ts | 198 +- src/crypto/algorithms/olm.ts | 39 +- src/crypto/api.ts | 4 +- src/crypto/backup.ts | 26 +- src/crypto/deviceinfo.ts | 61 +- src/crypto/index.ts | 528 ++-- src/crypto/keybackup.ts | 4 + src/crypto/olmlib.ts | 80 +- src/crypto/store/base.ts | 39 +- .../store/indexeddb-crypto-store-backend.ts | 51 +- src/crypto/store/indexeddb-crypto-store.ts | 214 +- src/crypto/store/localStorage-crypto-store.ts | 7 +- src/crypto/store/memory-crypto-store.ts | 51 +- src/crypto/verification/Base.ts | 17 +- src/crypto/verification/Error.ts | 2 - src/crypto/verification/IllegalMethod.ts | 5 - src/crypto/verification/QRCode.ts | 5 - src/crypto/verification/SAS.ts | 5 - src/crypto/verification/SASDecimal.ts | 4 +- .../verification/request/InRoomChannel.ts | 57 +- .../verification/request/ToDeviceChannel.ts | 51 +- .../request/VerificationRequest.ts | 58 +- src/embedded.ts | 2 +- src/event-mapper.ts | 3 + src/filter-component.ts | 37 +- src/filter.ts | 42 +- src/http-api/errors.ts | 25 +- src/http-api/fetch.ts | 102 +- src/http-api/index.ts | 22 +- src/http-api/interface.ts | 55 +- src/http-api/utils.ts | 18 +- src/indexeddb-helpers.ts | 6 +- src/indexeddb-worker.ts | 2 +- src/interactive-auth.ts | 166 +- src/logger.ts | 6 +- src/matrix.ts | 25 +- src/models/MSC3089Branch.ts | 30 +- src/models/MSC3089TreeSpace.ts | 58 +- src/models/event-context.ts | 25 +- src/models/event-status.ts | 1 - src/models/event-timeline-set.ts | 185 +- src/models/event-timeline.ts | 67 +- src/models/event.ts | 250 +- src/models/invites-ignorer.ts | 16 +- src/models/read-receipt.ts | 32 +- src/models/relations-container.ts | 19 +- src/models/relations.ts | 30 +- src/models/room-member.ts | 201 +- src/models/room-state.ts | 246 +- src/models/room-summary.ts | 20 +- src/models/room.ts | 540 ++-- src/models/search-result.ts | 14 +- src/models/thread.ts | 6 +- src/models/typed-event-emitter.ts | 4 +- src/models/user.ts | 203 +- src/pushprocessor.ts | 42 +- src/realtime-callbacks.ts | 9 +- src/rendezvous/MSC3906Rendezvous.ts | 8 +- src/rendezvous/RendezvousChannel.ts | 2 +- src/rendezvous/RendezvousTransport.ts | 4 +- src/room-hierarchy.ts | 13 +- src/scheduler.ts | 120 +- src/sliding-sync-sdk.ts | 23 +- src/sliding-sync.ts | 72 +- src/store/index.ts | 76 +- src/store/indexeddb-local-backend.ts | 57 +- src/store/indexeddb-remote-backend.ts | 27 +- src/store/indexeddb-store-worker.ts | 8 +- src/store/indexeddb.ts | 69 +- src/store/memory.ts | 98 +- src/store/stub.ts | 58 +- src/sync-accumulator.ts | 25 +- src/sync.ts | 112 +- src/timeline-window.ts | 94 +- src/utils.ts | 123 +- src/webrtc/audioContext.ts | 2 +- src/webrtc/call.ts | 126 +- src/webrtc/callEventHandler.ts | 10 + src/webrtc/callFeed.ts | 14 +- src/webrtc/groupCall.ts | 33 +- src/webrtc/mediaHandler.ts | 24 +- yarn.lock | 80 +- 111 files changed, 3970 insertions(+), 4772 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 3059df17f4d..c1cc4dc4caa 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,6 +2,7 @@ module.exports = { plugins: [ "matrix-org", "import", + "jsdoc", ], extends: [ "plugin:matrix-org/babel", @@ -45,7 +46,7 @@ module.exports = { // restrict EventEmitters to force callers to use TypedEventEmitter "no-restricted-imports": ["error", { name: "events", - message: "Please use TypedEventEmitter instead" + message: "Please use TypedEventEmitter instead", }], "import/no-restricted-paths": ["error", { @@ -61,6 +62,9 @@ module.exports = { files: [ "**/*.ts", ], + plugins: [ + "eslint-plugin-tsdoc", + ], extends: [ "plugin:matrix-org/typescript", ], @@ -84,6 +88,23 @@ module.exports = { "quotes": "off", // We use a `logger` intermediary module "no-console": "error", + + }, + }, { + // We don't need amazing docs in our spec files + files: [ + "src/**/*.ts", + ], + rules: { + "tsdoc/syntax": "error", + // We use some select jsdoc rules as the tsdoc linter has only one rule + "jsdoc/no-types": "error", + "jsdoc/empty-tags": "error", + "jsdoc/check-property-names": "error", + "jsdoc/check-values": "error", + // These need a bit more work before we can enable + // "jsdoc/check-param-names": "error", + // "jsdoc/check-indentation": "error", }, }, { files: [ diff --git a/package.json b/package.json index 76529468dc1..2685e5d2830 100644 --- a/package.json +++ b/package.json @@ -88,8 +88,8 @@ "@types/node": "18", "@types/sdp-transform": "^2.4.5", "@types/uuid": "7", - "@typescript-eslint/eslint-plugin": "^5.6.0", - "@typescript-eslint/parser": "^5.6.0", + "@typescript-eslint/eslint-plugin": "^5.45.0", + "@typescript-eslint/parser": "^5.45.0", "allchange": "^1.0.6", "babel-jest": "^29.0.0", "babelify": "^10.0.0", @@ -101,7 +101,9 @@ "eslint-config-google": "^0.14.0", "eslint-import-resolver-typescript": "^3.5.1", "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsdoc": "^39.6.4", "eslint-plugin-matrix-org": "^0.8.0", + "eslint-plugin-tsdoc": "^0.2.17", "eslint-plugin-unicorn": "^45.0.0", "exorcist": "^2.0.0", "fake-indexeddb": "^4.0.0", diff --git a/spec/TestClient.ts b/spec/TestClient.ts index 02d26b6a76f..8b54fd92ab9 100644 --- a/spec/TestClient.ts +++ b/spec/TestClient.ts @@ -105,7 +105,7 @@ export class TestClient { /** * stop the client - * @return {Promise} Resolves once the mock http backend has finished all pending flushes + * @returns Promise which resolves once the mock http backend has finished all pending flushes */ public async stop(): Promise { this.client.stopClient(); @@ -135,7 +135,7 @@ export class TestClient { * set up an expectation that the keys will be uploaded, and wait for * that to happen. * - * @returns {Promise} for the one-time keys + * @returns Promise for the one-time keys */ public awaitOneTimeKeyUpload(): Promise> { if (Object.keys(this.oneTimeKeys!).length != 0) { @@ -177,7 +177,7 @@ export class TestClient { * * We check that the query contains each of the users in `response`. * - * @param {Object} response response to the query. + * @param response - response to the query. */ public expectKeyQuery(response: IDownloadKeyResult) { this.httpBackend.when('POST', '/keys/query').respond( @@ -202,7 +202,7 @@ export class TestClient { /** * get the uploaded curve25519 device key * - * @return {string} base64 device key + * @returns base64 device key */ public getDeviceKey(): string { const keyId = 'curve25519:' + this.deviceId; @@ -212,7 +212,7 @@ export class TestClient { /** * get the uploaded ed25519 device key * - * @return {string} base64 device key + * @returns base64 device key */ public getSigningKey(): string { const keyId = 'ed25519:' + this.deviceId; diff --git a/spec/integ/devicelist-integ.spec.ts b/spec/integ/devicelist-integ.spec.ts index 5b980149b02..35af27e51dc 100644 --- a/spec/integ/devicelist-integ.spec.ts +++ b/spec/integ/devicelist-integ.spec.ts @@ -26,9 +26,7 @@ const ROOM_ID = "!room:id"; * get a /sync response which contains a single e2e room (ROOM_ID), with the * members given * - * @param {string[]} roomMembers - * - * @return {object} sync response + * @returns sync response */ function getSyncResponse(roomMembers: string[]) { const stateEvents = [ diff --git a/spec/integ/matrix-client-crypto.spec.ts b/spec/integ/matrix-client-crypto.spec.ts index ae58be1c91c..74366a21b6b 100644 --- a/spec/integ/matrix-client-crypto.spec.ts +++ b/spec/integ/matrix-client-crypto.spec.ts @@ -60,7 +60,7 @@ async function bobUploadsDeviceKeys(): Promise { /** * Set an expectation that querier will query uploader's keys; then flush the http request. * - * @return {promise} resolves once the http request has completed. + * @returns resolves once the http request has completed. */ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise { // can't query keys before bob has uploaded them @@ -83,7 +83,7 @@ const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient); /** * Set an expectation that ali will claim one of bob's keys; then flush the http request. * - * @return {promise} resolves once the http request has completed. + * @returns resolves once the http request has completed. */ async function expectAliClaimKeys(): Promise { const keys = await bobTestClient.awaitOneTimeKeyUpload(); @@ -151,7 +151,7 @@ const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client) * Ali sends a message, first claiming e2e keys. Set the expectations and * check the results. * - * @return {promise} which resolves to the ciphertext for Bob's device. + * @returns which resolves to the ciphertext for Bob's device. */ async function aliSendsFirstMessage(): Promise { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -168,7 +168,7 @@ async function aliSendsFirstMessage(): Promise { * Ali sends a message without first claiming e2e keys. Set the expectations * and check the results. * - * @return {promise} which resolves to the ciphertext for Bob's device. + * @returns which resolves to the ciphertext for Bob's device. */ async function aliSendsMessage(): Promise { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -183,7 +183,7 @@ async function aliSendsMessage(): Promise { * Bob sends a message, first querying (but not claiming) e2e keys. Set the * expectations and check the results. * - * @return {promise} which resolves to the ciphertext for Ali's device. + * @returns which resolves to the ciphertext for Ali's device. */ async function bobSendsReplyMessage(): Promise { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -198,7 +198,7 @@ async function bobSendsReplyMessage(): Promise { /** * Set an expectation that Ali will send a message, and flush the request * - * @return {promise} which resolves to the ciphertext for Bob's device. + * @returns which resolves to the ciphertext for Bob's device. */ async function expectAliSendMessageRequest(): Promise { const content = await expectSendMessageRequest(aliTestClient.httpBackend); @@ -212,7 +212,7 @@ async function expectAliSendMessageRequest(): Promise { /** * Set an expectation that Bob will send a message, and flush the request * - * @return {promise} which resolves to the ciphertext for Bob's device. + * @returns which resolves to the ciphertext for Bob's device. */ async function expectBobSendMessageRequest(): Promise { const content = await expectSendMessageRequest(bobTestClient.httpBackend); @@ -321,8 +321,7 @@ async function recvMessage( * Send an initial sync response to the client (which just includes the member * list for our test room). * - * @param {TestClient} testClient - * @returns {Promise} which resolves when the sync has been flushed. + * @returns which resolves when the sync has been flushed. */ function firstSync(testClient: TestClient): Promise { // send a sync response including our test room. diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index dbb891449cc..5ddcb0aa01c 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -1658,8 +1658,8 @@ describe("MatrixClient syncing", () => { /** * waits for the MatrixClient to emit one or more 'sync' events. * - * @param {Number?} numSyncs number of syncs to wait for - * @returns {Promise} promise which resolves after the sync events have happened + * @param numSyncs - number of syncs to wait for + * @returns promise which resolves after the sync events have happened */ function awaitSyncEvent(numSyncs?: number) { return utils.syncPromise(client!, numSyncs); diff --git a/spec/integ/megolm-integ.spec.ts b/spec/integ/megolm-integ.spec.ts index 89c06426617..73a1999363b 100644 --- a/spec/integ/megolm-integ.spec.ts +++ b/spec/integ/megolm-integ.spec.ts @@ -220,8 +220,8 @@ describe("megolm", () => { * Get the device keys for testOlmAccount in a format suitable for a * response to /keys/query * - * @param {string} userId The user ID to query for - * @returns {IDownloadKeyResult} The fake query response + * @param userId - The user ID to query for + * @returns The fake query response */ function getTestKeysQueryResponse(userId: string): IDownloadKeyResult { const testE2eKeys = JSON.parse(testOlmAccount.identity_keys()); @@ -248,8 +248,8 @@ describe("megolm", () => { * Get a one-time key for testOlmAccount in a format suitable for a * response to /keys/claim - * @param {string} userId The user ID to query for - * @returns {IClaimOTKsResult} The fake key claim response + * @param userId - The user ID to query for + * @returns The fake key claim response */ function getTestKeysClaimResponse(userId: string): IClaimOTKsResult { testOlmAccount.generate_one_time_keys(1); diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index c2a4b8e98fc..d90c0236a56 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -1446,11 +1446,11 @@ function timeout(delayMs: number, reason: string): { promise: Promise, ca /** * Listen until a callback returns data. - * @param {EventEmitter} emitter The event emitter - * @param {string} eventName The event to listen for - * @param {function} callback The callback which will be invoked when events fire. Return something truthy from this to resolve the promise. - * @param {number} timeoutMs The number of milliseconds to wait for the callback to return data. Default: 500ms. - * @returns {Promise} A promise which will be resolved when the callback returns data. If the callback throws or the timeout is reached, + * @param emitter - The event emitter + * @param eventName - The event to listen for + * @param callback - The callback which will be invoked when events fire. Return something truthy from this to resolve the promise. + * @param timeoutMs - The number of milliseconds to wait for the callback to return data. Default: 500ms. + * @returns A promise which will be resolved when the callback returns data. If the callback throws or the timeout is reached, * the promise is rejected. */ function listenUntil( diff --git a/spec/test-utils/test-utils.ts b/spec/test-utils/test-utils.ts index 3dd9c037844..814f9b15e40 100644 --- a/spec/test-utils/test-utils.ts +++ b/spec/test-utils/test-utils.ts @@ -13,9 +13,9 @@ import { eventMapperFor } from "../../src/event-mapper"; /** * Return a promise that is resolved when the client next emits a * SYNCING event. - * @param {Object} client The client - * @param {Number=} count Number of syncs to wait for (default 1) - * @return {Promise} Resolves once the client has emitted a SYNCING event + * @param client - The client + * @param count - Number of syncs to wait for (default 1) + * @returns Promise which resolves once the client has emitted a SYNCING event */ export function syncPromise(client: MatrixClient, count = 1): Promise { if (count <= 0) { @@ -41,9 +41,9 @@ export function syncPromise(client: MatrixClient, count = 1): Promise { /** * Create a spy for an object and automatically spy its methods. - * @param {*} constr The class constructor (used with 'new') - * @param {string} name The name of the class - * @return {Object} An instantiated object with spied methods/properties. + * @param constr - The class constructor (used with 'new') + * @param name - The name of the class + * @returns An instantiated object with spied methods/properties. */ export function mock(constr: { new(...args: any[]): T }, name: string): T { // Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/ @@ -84,15 +84,15 @@ interface IEventOpts { let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events /** * Create an Event. - * @param {Object} opts Values for the event. - * @param {string} opts.type The event.type - * @param {string} opts.room The event.room_id - * @param {string} opts.sender The event.sender - * @param {string} opts.skey Optional. The state key (auto inserts empty string) - * @param {Object} opts.content The event.content - * @param {boolean} opts.event True to make a MatrixEvent. - * @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters. - * @return {Object} a JSON object representing this event. + * @param opts - Values for the event. + * @param opts.type - The event.type + * @param opts.room - The event.room_id + * @param opts.sender - The event.sender + * @param opts.skey - Optional. The state key (auto inserts empty string) + * @param opts.content - The event.content + * @param opts.event - True to make a MatrixEvent. + * @param client - If passed along with opts.event=true will be used to set up re-emitters. + * @returns a JSON object representing this event. */ export function mkEvent(opts: IEventOpts & { event: true }, client?: MatrixClient): MatrixEvent; export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): Partial; @@ -160,8 +160,8 @@ interface IPresenceOpts { /** * Create an m.presence event. - * @param {Object} opts Values for the presence. - * @return {Object|MatrixEvent} The event + * @param opts - Values for the presence. + * @returns The event */ export function mkPresence(opts: IPresenceOpts & { event: true }): MatrixEvent; export function mkPresence(opts: IPresenceOpts & { event?: false }): Partial; @@ -193,16 +193,16 @@ interface IMembershipOpts { /** * Create an m.room.member event. - * @param {Object} opts Values for the membership. - * @param {string} opts.room The room ID for the event. - * @param {string} opts.mship The content.membership for the event. - * @param {string} opts.sender The sender user ID for the event. - * @param {string} opts.skey The target user ID for the event if applicable + * @param opts - Values for the membership. + * @param opts.room - The room ID for the event. + * @param opts.mship - The content.membership for the event. + * @param opts.sender - The sender user ID for the event. + * @param opts.skey - The target user ID for the event if applicable * e.g. for invites/bans. - * @param {string} opts.name The content.displayname for the event. - * @param {string} opts.url The content.avatar_url for the event. - * @param {boolean} opts.event True to make a MatrixEvent. - * @return {Object|MatrixEvent} The event + * @param opts.name - The content.displayname for the event. + * @param opts.url - The content.avatar_url for the event. + * @param opts.event - True to make a MatrixEvent. + * @returns The event */ export function mkMembership(opts: IMembershipOpts & { event: true }): MatrixEvent; export function mkMembership(opts: IMembershipOpts & { event?: false }): Partial; @@ -250,13 +250,13 @@ export interface IMessageOpts { /** * Create an m.room.message event. - * @param {Object} opts Values for the message - * @param {string} opts.room The room ID for the event. - * @param {string} opts.user The user ID for the event. - * @param {string} opts.msg Optional. The content.body for the event. - * @param {boolean} opts.event True to make a MatrixEvent. - * @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters. - * @return {Object|MatrixEvent} The event + * @param opts - Values for the message + * @param opts.room - The room ID for the event. + * @param opts.user - The user ID for the event. + * @param opts.msg - Optional. The content.body for the event. + * @param opts.event - True to make a MatrixEvent. + * @param client - If passed along with opts.event=true will be used to set up re-emitters. + * @returns The event */ export function mkMessage(opts: IMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent; export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): Partial; @@ -290,14 +290,14 @@ interface IReplyMessageOpts extends IMessageOpts { /** * Create a reply message. * - * @param {Object} opts Values for the message - * @param {string} opts.room The room ID for the event. - * @param {string} opts.user The user ID for the event. - * @param {string} opts.msg Optional. The content.body for the event. - * @param {MatrixEvent} opts.replyToMessage The replied message - * @param {boolean} opts.event True to make a MatrixEvent. - * @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters. - * @return {Object|MatrixEvent} The event + * @param opts - Values for the message + * @param opts.room - The room ID for the event. + * @param opts.user - The user ID for the event. + * @param opts.msg - Optional. The content.body for the event. + * @param opts.replyToMessage - The replied message + * @param opts.event - True to make a MatrixEvent. + * @param client - If passed along with opts.event=true will be used to set up re-emitters. + * @returns The event */ export function mkReplyMessage(opts: IReplyMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent; export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): Partial; @@ -329,8 +329,6 @@ export function mkReplyMessage( /** * A mock implementation of webstorage - * - * @constructor */ export class MockStorageApi implements Storage { private data: Record = {}; @@ -363,8 +361,7 @@ export class MockStorageApi implements Storage { /** * If an event is being decrypted, wait for it to finish being decrypted. * - * @param {MatrixEvent} event - * @returns {Promise} promise which resolves (to `event`) when the event has been decrypted + * @returns promise which resolves (to `event`) when the event has been decrypted */ export async function awaitDecryption( event: MatrixEvent, { waitOnDecryptionFailure = false } = {}, diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index c820f37a074..ad74a285352 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -16,7 +16,6 @@ limitations under the License. /** * This is an internal module. See {@link MatrixClient} for the public class. - * @module client */ import { mocked } from "jest-mock"; diff --git a/src/@types/IIdentityServerProvider.ts b/src/@types/IIdentityServerProvider.ts index 7b905e316b3..05793d53a5d 100644 --- a/src/@types/IIdentityServerProvider.ts +++ b/src/@types/IIdentityServerProvider.ts @@ -18,7 +18,7 @@ export interface IIdentityServerProvider { /** * Gets an access token for use against the identity server, * for the associated client. - * @returns {Promise} Resolves to the access token. + * @returns Promise which resolves to the access token. */ getAccessToken(): Promise; } diff --git a/src/@types/beacon.ts b/src/@types/beacon.ts index 6da17061e61..ea8a9c8eb8f 100644 --- a/src/@types/beacon.ts +++ b/src/@types/beacon.ts @@ -35,7 +35,8 @@ import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location"; * To achieve an arbitrary number of only owner-writable state events * we introduce a variable suffix to the event type * - * Eg + * @example + * ``` * { * "type": "m.beacon_info.@matthew:matrix.org.1", * "state_key": "@matthew:matrix.org", @@ -58,6 +59,7 @@ import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location"; * // more content as described below * } * } + * ``` */ /** @@ -78,20 +80,23 @@ export type MBeaconInfoContent = { /** * m.beacon_info Event example from the spec * https://github.com/matrix-org/matrix-spec-proposals/pull/3672 + * @example + * ``` * { - "type": "m.beacon_info", - "state_key": "@matthew:matrix.org", - "content": { - "m.beacon_info": { - "description": "The Matthew Tracker", // same as an `m.location` description - "timeout": 86400000, // how long from the last event until we consider the beacon inactive in milliseconds - }, - "m.ts": 1436829458432, // creation timestamp of the beacon on the client - "m.asset": { - "type": "m.self" // the type of asset being tracked as per MSC3488 - } - } -} + * "type": "m.beacon_info", + * "state_key": "@matthew:matrix.org", + * "content": { + * "m.beacon_info": { + * "description": "The Matthew Tracker", // same as an `m.location` description + * "timeout": 86400000, // how long from the last event until we consider the beacon inactive in milliseconds + * }, + * "m.ts": 1436829458432, // creation timestamp of the beacon on the client + * "m.asset": { + * "type": "m.self" // the type of asset being tracked as per MSC3488 + * } + * } + * } + * ``` */ /** @@ -107,22 +112,24 @@ export type MBeaconInfoEventContent = & /** * m.beacon event example * https://github.com/matrix-org/matrix-spec-proposals/pull/3672 - * + * @example + * ``` * { - "type": "m.beacon", - "sender": "@matthew:matrix.org", - "content": { - "m.relates_to": { // from MSC2674: https://github.com/matrix-org/matrix-doc/pull/2674 - "rel_type": "m.reference", // from MSC3267: https://github.com/matrix-org/matrix-doc/pull/3267 - "event_id": "$beacon_info" - }, - "m.location": { - "uri": "geo:51.5008,0.1247;u=35", - "description": "Arbitrary beacon information" - }, - "m.ts": 1636829458432, - } -} + * "type": "m.beacon", + * "sender": "@matthew:matrix.org", + * "content": { + * "m.relates_to": { // from MSC2674: https://github.com/matrix-org/matrix-doc/pull/2674 + * "rel_type": "m.reference", // from MSC3267: https://github.com/matrix-org/matrix-doc/pull/3267 + * "event_id": "$beacon_info" + * }, + * "m.location": { + * "uri": "geo:51.5008,0.1247;u=35", + * "description": "Arbitrary beacon information" + * }, + * "m.ts": 1636829458432, + * } + * } + * ``` */ /** diff --git a/src/@types/event.ts b/src/@types/event.ts index ab07c01d776..d8bb4cc3081 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -171,17 +171,21 @@ export const UNSTABLE_MSC2716_MARKER = new UnstableValue("m.room.marker", "org.m * eventual removal. * * Schema (TypeScript): + * ``` * { * service_members?: string[] * } + * ``` * - * Example: + * @example + * ``` * { * "service_members": [ * "@helperbot:localhost", * "@reminderbot:alice.tdl" * ] * } + * ``` */ export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue( "io.element.functional_members", diff --git a/src/@types/requests.ts b/src/@types/requests.ts index 936ace5b46e..75296940a9c 100644 --- a/src/@types/requests.ts +++ b/src/@types/requests.ts @@ -54,17 +54,31 @@ export interface ISendEventResponse { } export interface IPresenceOpts { + // One of "online", "offline" or "unavailable" presence: "online" | "offline" | "unavailable"; + // The status message to attach. status_msg?: string; } export interface IPaginateOpts { + // true to fill backwards, false to go forwards backwards?: boolean; + // number of events to request limit?: number; } export interface IGuestAccessOpts { + /** + * True to allow guests to join this room. This + * implicitly gives guests write access. If false or not given, guests are + * explicitly forbidden from joining the room. + */ allowJoin: boolean; + /** + * True to set history visibility to + * be world_readable. This gives guests read access *from this point forward*. + * If false or not given, history visibility is not modified. + */ allowRead: boolean; } @@ -74,7 +88,9 @@ export interface ISearchOpts { } export interface IEventSearchOpts { + // a JSON filter object to pass in the request filter?: IRoomEventFilter; + // the term to search for term: string; } @@ -92,9 +108,13 @@ export interface ICreateRoomStateEvent { } export interface ICreateRoomOpts { + // The alias localpart to assign to this room. room_alias_name?: string; + // Either 'public' or 'private'. visibility?: Visibility; + // The name to give this room. name?: string; + // The topic to give this room. topic?: string; preset?: Preset; power_level_content_override?: { @@ -111,6 +131,7 @@ export interface ICreateRoomOpts { }; creation_content?: object; initial_state?: ICreateRoomStateEvent[]; + // A list of user IDs to invite to this room. invite?: string[]; invite_3pid?: IInvite3PID[]; is_direct?: boolean; @@ -121,7 +142,10 @@ export interface IRoomDirectoryOptions { server?: string; limit?: number; since?: string; + + // Filter parameters filter?: { + // String to search for generic_search_term?: string; room_types?: Array; }; diff --git a/src/@types/topic.ts b/src/@types/topic.ts index 0d2708b2e50..5b66e07c46f 100644 --- a/src/@types/topic.ts +++ b/src/@types/topic.ts @@ -21,10 +21,9 @@ import { UnstableValue } from "../NamespacedValue"; /** * Extensible topic event type based on MSC3765 * https://github.com/matrix-org/matrix-spec-proposals/pull/3765 - */ - -/** - * Eg + * + * @example + * ``` * { * "type": "m.room.topic, * "state_key": "", @@ -39,6 +38,7 @@ import { UnstableValue } from "../NamespacedValue"; * }], * } * } + * ``` */ /** diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index fe2b6e383f3..35728d1e21d 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -15,8 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** @module auto-discovery */ - import { IClientWellKnown, IWellKnownConfig } from "./client"; import { logger } from './logger'; import { MatrixError, Method, timeoutSignal } from "./http-api"; @@ -87,8 +85,6 @@ export class AutoDiscovery { /** * The auto discovery failed. The client is expected to communicate * the error to the user and refuse logging in. - * @return {string} - * @constructor */ public static readonly FAIL_ERROR = AutoDiscoveryAction.FAIL_ERROR; @@ -98,8 +94,6 @@ export class AutoDiscovery { * action it would for PROMPT while also warning the user about * what went wrong. The client may also treat this the same as * a FAIL_ERROR state. - * @return {string} - * @constructor */ public static readonly FAIL_PROMPT = AutoDiscoveryAction.FAIL_PROMPT; @@ -107,15 +101,11 @@ export class AutoDiscovery { * The auto discovery didn't fail but did not find anything of * interest. The client is expected to prompt the user for more * information, or fail if it prefers. - * @return {string} - * @constructor */ public static readonly PROMPT = AutoDiscoveryAction.PROMPT; /** * The auto discovery was successful. - * @return {string} - * @constructor */ public static readonly SUCCESS = AutoDiscoveryAction.SUCCESS; @@ -125,9 +115,9 @@ export class AutoDiscovery { * and identity server URL the client would want. Additional details * may also be included, and will be transparently brought into the * response object unaltered. - * @param {object} wellknown The configuration object itself, as returned + * @param wellknown - The configuration object itself, as returned * by the .well-known auto-discovery endpoint. - * @return {Promise} Resolves to the verified + * @returns Promise which resolves to the verified * configuration, which may include error states. Rejects on unexpected * failure, not when verification fails. */ @@ -284,9 +274,9 @@ export class AutoDiscovery { * and identity server URL the client would want. Additional details * may also be discovered, and will be transparently included in the * response object unaltered. - * @param {string} domain The homeserver domain to perform discovery + * @param domain - The homeserver domain to perform discovery * on. For example, "matrix.org". - * @return {Promise} Resolves to the discovered + * @returns Promise which resolves to the discovered * configuration, which may include error states. Rejects on unexpected * failure, not when discovery fails. */ @@ -354,8 +344,8 @@ export class AutoDiscovery { * Gets the raw discovery client configuration for the given domain name. * Should only be used if there's no validation to be done on the resulting * object, otherwise use findClientConfig(). - * @param {string} domain The domain to get the client config for. - * @returns {Promise} Resolves to the domain's client config. Can + * @param domain - The domain to get the client config for. + * @returns Promise which resolves to the domain's client config. Can * be an empty object. */ public static async getRawClientConfig(domain?: string): Promise { @@ -374,9 +364,9 @@ export class AutoDiscovery { * Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and * is suitable for the requirements laid out by .well-known auto discovery. * If valid, the URL will also be stripped of any trailing slashes. - * @param {string} url The potentially invalid URL to sanitize. - * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid. - * @private + * @param url - The potentially invalid URL to sanitize. + * @returns The sanitized URL or a falsey value if the URL is invalid. + * @internal */ private static sanitizeWellKnownUrl(url?: string | null): string | false { if (!url) return false; @@ -430,9 +420,9 @@ export class AutoDiscovery { * action: One of SUCCESS, IGNORE, or FAIL_PROMPT. * reason: Relatively human-readable description of what went wrong. * error: The actual Error, if one exists. - * @param {string} url The URL to fetch a JSON object from. - * @return {Promise} Resolves to the returned state. - * @private + * @param url - The URL to fetch a JSON object from. + * @returns Promise which resolves to the returned state. + * @internal */ private static async fetchWellKnownObject(url: string): Promise { let response: Response; diff --git a/src/client.ts b/src/client.ts index 6fb74ca173c..fba259a5671 100644 --- a/src/client.ts +++ b/src/client.ts @@ -16,7 +16,6 @@ limitations under the License. /** * This is an internal module. See {@link MatrixClient} for the public class. - * @module client */ import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk"; @@ -258,7 +257,10 @@ export interface ICreateClientOpts { /** * A store to be used for end-to-end crypto session data. If not specified, * end-to-end crypto will be disabled. The `createClient` helper will create - * a default store if needed. + * a default store if needed. Calls the factory supplied to + * {@link setCryptoStoreFactory} if unspecified; or if no factory has been + * specified, uses a default implementation (indexeddb in the browser, + * in-memory otherwise). */ cryptoStore?: CryptoStore; @@ -266,7 +268,7 @@ export interface ICreateClientOpts { * The scheduler to use. If not * specified, this client will not retry requests on failure. This client * will supply its own processing function to - * {@link module:scheduler~MatrixScheduler#setProcessFunction}. + * {@link MatrixScheduler#setProcessFunction}. */ scheduler?: MatrixScheduler; @@ -311,8 +313,8 @@ export interface ICreateClientOpts { /** * Set to true to enable - * improved timeline support ({@link module:client~MatrixClient#getEventTimeline getEventTimeline}). It is - * disabled by default for compatibility with older clients - in particular to + * improved timeline support, see {@link MatrixClient#getEventTimeline}. + * It is disabled by default for compatibility with older clients - in particular to * maintain support for back-paginating the live timeline after a '/sync' * result with a gap. */ @@ -321,7 +323,7 @@ export interface ICreateClientOpts { /** * Extra query parameters to append * to all requests with this client. Useful for application services which require - * ?user_id=. + * `?user_id=`. */ queryParams?: Record; @@ -395,12 +397,12 @@ export enum PendingEventOrdering { export interface IStartClientOpts { /** - * The event limit= to apply to initial sync. Default: 8. + * The event `limit=` to apply to initial sync. Default: 8. */ initialSyncLimit?: number; /** - * True to put archived=true on the /initialSync request. Default: false. + * True to put `archived=true on the /initialSync` request. Default: false. */ includeArchivedRooms?: boolean; @@ -411,8 +413,8 @@ export interface IStartClientOpts { /** * Controls where pending messages appear in a room's timeline. If "chronological", messages will - * appear in the timeline when the call to sendEvent was made. If "detached", - * pending messages will appear in a separate list, accessbile via {@link module:models/room#getPendingEvents}. + * appear in the timeline when the call to `sendEvent` was made. If "detached", + * pending messages will appear in a separate list, accessbile via {@link Room#getPendingEvents}. * Default: "chronological". */ pendingEventOrdering?: PendingEventOrdering; @@ -456,7 +458,14 @@ export interface IStartClientOpts { } export interface IStoredClientOpts extends IStartClientOpts { + // Crypto manager crypto?: Crypto; + /** + * A function which is called + * with a room ID and returns a boolean. It should return 'true' if the SDK can + * SAFELY remove events from this room. It may not be safe to remove events if + * there are other references to the timelines for this room. + */ canResetEntireTimeline: ResetTimelineCallback; } @@ -916,13 +925,184 @@ export type EmittedEvents = ClientEvent | BeaconEvent; export type ClientEventHandlerMap = { + /** + * Fires whenever the SDK's syncing state is updated. The state can be one of: + *
    + * + *
  • PREPARED: The client has synced with the server at least once and is + * ready for methods to be called on it. This will be immediately followed by + * a state of SYNCING. This is the equivalent of "syncComplete" in the + * previous API.
  • + * + *
  • CATCHUP: The client has detected the connection to the server might be + * available again and will now try to do a sync again. As this sync might take + * a long time (depending how long ago was last synced, and general server + * performance) the client is put in this mode so the UI can reflect trying + * to catch up with the server after losing connection.
  • + * + *
  • SYNCING : The client is currently polling for new events from the server. + * This will be called after processing latest events from a sync.
  • + * + *
  • ERROR : The client has had a problem syncing with the server. If this is + * called before PREPARED then there was a problem performing the initial + * sync. If this is called after PREPARED then there was a problem polling + * the server for updates. This may be called multiple times even if the state is + * already ERROR. This is the equivalent of "syncError" in the previous + * API.
  • + * + *
  • RECONNECTING: The sync connection has dropped, but not (yet) in a way that + * should be considered erroneous. + *
  • + * + *
  • STOPPED: The client has stopped syncing with server due to stopClient + * being called. + *
  • + *
+ * State transition diagram: + * ``` + * +---->STOPPED + * | + * +----->PREPARED -------> SYNCING <--+ + * | ^ | ^ | + * | CATCHUP ----------+ | | | + * | ^ V | | + * null ------+ | +------- RECONNECTING | + * | V V | + * +------->ERROR ---------------------+ + * + * NB: 'null' will never be emitted by this event. + * + * ``` + * Transitions: + *
    + * + *
  • `null -> PREPARED` : Occurs when the initial sync is completed + * first time. This involves setting up filters and obtaining push rules. + * + *
  • `null -> ERROR` : Occurs when the initial sync failed first time. + * + *
  • `ERROR -> PREPARED` : Occurs when the initial sync succeeds + * after previously failing. + * + *
  • `PREPARED -> SYNCING` : Occurs immediately after transitioning + * to PREPARED. Starts listening for live updates rather than catching up. + * + *
  • `SYNCING -> RECONNECTING` : Occurs when the live update fails. + * + *
  • `RECONNECTING -> RECONNECTING` : Can occur if the update calls + * continue to fail, but the keepalive calls (to /versions) succeed. + * + *
  • `RECONNECTING -> ERROR` : Occurs when the keepalive call also fails + * + *
  • `ERROR -> SYNCING` : Occurs when the client has performed a + * live update after having previously failed. + * + *
  • `ERROR -> ERROR` : Occurs when the client has failed to keepalive + * for a second time or more.
  • + * + *
  • `SYNCING -> SYNCING` : Occurs when the client has performed a live + * update. This is called after processing.
  • + * + *
  • `* -> STOPPED` : Occurs once the client has stopped syncing or + * trying to sync after stopClient has been called.
  • + *
+ * + * @param state - An enum representing the syncing state. One of "PREPARED", + * "SYNCING", "ERROR", "STOPPED". + * + * @param prevState - An enum representing the previous syncing state. + * One of "PREPARED", "SYNCING", "ERROR", "STOPPED" or null. + * + * @param data - Data about this transition. + * + * @example + * ``` + * matrixClient.on("sync", function(state, prevState, data) { + * switch (state) { + * case "ERROR": + * // update UI to say "Connection Lost" + * break; + * case "SYNCING": + * // update UI to remove any "Connection Lost" message + * break; + * case "PREPARED": + * // the client instance is ready to be queried. + * var rooms = matrixClient.getRooms(); + * break; + * } + * }); + * ``` + */ [ClientEvent.Sync]: (state: SyncState, lastState: SyncState | null, data?: ISyncStateData) => void; + /** + * Fires whenever the SDK receives a new event. + *

+ * This is only fired for live events received via /sync - it is not fired for + * events received over context, search, or pagination APIs. + * + * @param event - The matrix event which caused this event to fire. + * @example + * ``` + * matrixClient.on("event", function(event){ + * var sender = event.getSender(); + * }); + * ``` + */ [ClientEvent.Event]: (event: MatrixEvent) => void; + /** + * Fires whenever the SDK receives a new to-device event. + * @param event - The matrix event which caused this event to fire. + * @example + * ``` + * matrixClient.on("toDeviceEvent", function(event){ + * var sender = event.getSender(); + * }); + * ``` + */ [ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void; + /** + * Fires whenever new user-scoped account_data is added. + * @param event - The event describing the account_data just added + * @param event - The previous account data, if known. + * @example + * ``` + * matrixClient.on("accountData", function(event, oldEvent){ + * myAccountData[event.type] = event.content; + * }); + * ``` + */ [ClientEvent.AccountData]: (event: MatrixEvent, lastEvent?: MatrixEvent) => void; + /** + * Fires whenever a new Room is added. This will fire when you are invited to a + * room, as well as when you join a room. This event is experimental and + * may change. + * @param room - The newly created, fully populated room. + * @example + * ``` + * matrixClient.on("Room", function(room){ + * var roomId = room.roomId; + * }); + * ``` + */ [ClientEvent.Room]: (room: Room) => void; + /** + * Fires whenever a Room is removed. This will fire when you forget a room. + * This event is experimental and may change. + * @param roomId - The deleted room ID. + * @example + * ``` + * matrixClient.on("deleteRoom", function(roomId){ + * // update UI from getRooms() + * }); + * ``` + */ [ClientEvent.DeleteRoom]: (roomId: string) => void; [ClientEvent.SyncUnexpectedError]: (error: Error) => void; + /** + * Fires when the client .well-known info is fetched. + * + * @param data - The JSON object returned by the server + */ [ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void; [ClientEvent.ReceivedVoipEvent]: (event: MatrixEvent) => void; [ClientEvent.TurnServers]: (servers: ITurnServer[]) => void; @@ -1185,10 +1365,10 @@ export class MatrixClient extends TypedEventEmitter { if (this.clientRunning) { @@ -1302,9 +1482,9 @@ export class MatrixClient extends TypedEventEmitter} Resolves to undefined if a device could not be dehydrated, or + * @returns Promise which resolves to undefined if a device could not be dehydrated, or * to the new device ID if the dehydration was successful. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Rejects: with an error response. */ public async rehydrateDevice(): Promise { if (this.crypto) { @@ -1379,7 +1559,7 @@ export class MatrixClient extends TypedEventEmitter { try { @@ -1401,12 +1581,12 @@ export class MatrixClient extends TypedEventEmitter} the device id of the newly created dehydrated device + * @returns the device id of the newly created dehydrated device */ public async createDehydratedDevice( key: Uint8Array, @@ -1458,7 +1638,7 @@ export class MatrixClient extends TypedEventEmitter { if (this.clientRunning) { @@ -1477,7 +1657,7 @@ export class MatrixClient extends TypedEventEmitterThis method is experimental * and may change without warning. - * @param {boolean} guest True if this is a guest account. + * @param guest - True if this is a guest account. */ public setGuest(guest: boolean): void { // EXPERIMENTAL: @@ -1697,7 +1874,7 @@ export class MatrixClient extends TypedEventEmitterexplicitly attempts to retry their lost connection. * Will also retry any outbound to-device messages currently in the queue to be sent * (retries of regular outgoing events are handled separately, per-event). - * @return {boolean} True if this resulted in a request being retried. + * @returns True if this resulted in a request being retried. */ public retryImmediately(): boolean { // don't await for this promise: we just want to kick it off @@ -1719,7 +1896,7 @@ export class MatrixClient extends TypedEventEmitter { const now = new Date().getTime(); @@ -1870,7 +2046,7 @@ export class MatrixClient extends TypedEventEmitterdeviceId->{@link - * module:crypto~DeviceInfo|DeviceInfo}. + * @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo} */ public downloadKeys( userIds: string[], @@ -1925,9 +2100,9 @@ export class MatrixClient extends TypedEventEmitter { const prom = this.setDeviceVerification(userId, deviceId, verified, null, null); @@ -1980,16 +2156,17 @@ export class MatrixClient extends TypedEventEmitter { return this.setDeviceVerification(userId, deviceId, null, blocked, null); @@ -1998,16 +2175,17 @@ export class MatrixClient extends TypedEventEmitter { return this.setDeviceVerification(userId, deviceId, null, null, known); @@ -2029,10 +2207,10 @@ export class MatrixClient extends TypedEventEmitter} resolves to a VerificationRequest + * @returns resolves to a VerificationRequest * when the request has been sent to the other party. */ public requestVerificationDM(userId: string, roomId: string): Promise { @@ -2045,9 +2223,9 @@ export class MatrixClient extends TypedEventEmitter} resolves to a VerificationRequest + * @returns resolves to a VerificationRequest * when the request has been sent to the other party. */ public requestVerification(userId: string, devices?: string[]): Promise { @@ -2090,11 +2268,11 @@ export class MatrixClient extends TypedEventEmitter { @@ -2116,7 +2294,7 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2263,9 +2438,9 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2326,15 +2501,6 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2350,7 +2516,7 @@ export class MatrixClient extends TypedEventEmitter} Resolves to the number of sessions requiring backup + * @returns Promise which resolves to the number of sessions requiring backup */ public countSessionsNeedingBackup(): Promise { if (!this.crypto) { @@ -2385,8 +2551,8 @@ export class MatrixClient extends TypedEventEmitter} Object with public key metadata, encoded private + * @returns Object with public key metadata, encoded private * recovery key which should be disposed of after displaying to the user, * and raw private key to avoid round tripping if needed. */ @@ -2427,7 +2593,7 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2449,7 +2615,6 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2463,14 +2628,14 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2504,9 +2669,9 @@ export class MatrixClient extends TypedEventEmitter { @@ -2521,9 +2686,9 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2537,8 +2702,8 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2585,7 +2750,7 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2601,9 +2766,9 @@ export class MatrixClient extends TypedEventEmitter} + * @param event - event to be checked */ public async getEventSenderDeviceInfo(event: MatrixEvent): Promise { if (!this.crypto) { @@ -2629,10 +2792,10 @@ export class MatrixClient extends TypedEventEmitter { const device = await this.getEventSenderDeviceInfo(event); @@ -2644,9 +2807,9 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2671,9 +2834,9 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2685,9 +2848,9 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2698,8 +2861,8 @@ export class MatrixClient extends TypedEventEmitter} Promise which + * @returns Promise which * resolves once the message has been encrypted and sent to the given - * userDeviceMap, and returns the { contentMap, deviceInfoByDeviceId } + * userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }` * of the successfully sent messages. */ public encryptAndSendToDevices( @@ -2750,7 +2912,7 @@ export class MatrixClient extends TypedEventEmitter { @@ -2779,12 +2941,9 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2797,7 +2956,7 @@ export class MatrixClient extends TypedEventEmitter} Information object from API or null + * @returns Information object from API or null */ public async getKeyBackupVersion(): Promise { let res: IKeyBackupInfo; @@ -2832,14 +2991,7 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { @@ -2849,7 +3001,7 @@ export class MatrixClient extends TypedEventEmitter} Resolves when complete. + * @param info - Backup information object as returned by getKeyBackupVersion + * @returns Promise which resolves when complete. */ public enableKeyBackup(info: IKeyBackupInfo): Promise { if (!this.crypto) { @@ -2890,17 +3042,13 @@ export class MatrixClient extends TypedEventEmitter} Object that can be passed to createKeyBackupVersion and + * @returns Object that can be passed to createKeyBackupVersion and * additionally has a 'recovery_key' member with the user-facing recovery key string. */ - // TODO: Verify types public async prepareKeyBackupVersion( password?: string | Uint8Array | null, opts: IKeyBackupPrepareOpts = { secureSecretStorage: false }, @@ -2929,7 +3077,7 @@ export class MatrixClient extends TypedEventEmitter} map of key name to key info the secret is + * @returns map of key name to key info the secret is * encrypted with, or null if it is not present or not encrypted with a * trusted key */ @@ -2941,8 +3089,8 @@ export class MatrixClient extends TypedEventEmitter} Object with 'version' param indicating the version created + * @param info - Info object from prepareKeyBackupVersion + * @returns Object with 'version' param indicating the version created */ public async createKeyBackupVersion(info: IKeyBackupInfo): Promise { if (!this.crypto) { @@ -3036,11 +3184,11 @@ export class MatrixClient extends TypedEventEmitter} Resolves to the number of sessions requiring a backup. + * @returns Promise which resolves to the number of sessions requiring a backup. */ public flagAllGroupSessionsForBackup(): Promise { if (!this.crypto) { @@ -3118,9 +3266,9 @@ export class MatrixClient extends TypedEventEmitter} key backup key + * @param password - Passphrase + * @param backupInfo - Backup metadata from `checkKeyBackup` + * @returns key backup key */ public keyBackupKeyFromPassword(password: string, backupInfo: IKeyBackupInfo): Promise { return keyFromAuthData(backupInfo.auth_data, password); @@ -3132,8 +3280,8 @@ export class MatrixClient extends TypedEventEmitter} Status of restoration with `total` and `imported` + * @param backupInfo - Backup metadata from `checkKeyBackup` + * @param opts - Optional params such as callbacks + * @returns Status of restoration with `total` and `imported` * key counts. */ public async restoreKeyBackupWithPassword( @@ -3188,13 +3336,13 @@ export class MatrixClient extends TypedEventEmitter} Status of restoration with `total` and `imported` + * @param opts - Optional params such as callbacks + * @returns Status of restoration with `total` and `imported` * key counts. */ public async restoreKeyBackupWithSecretStorage( @@ -3223,15 +3371,15 @@ export class MatrixClient extends TypedEventEmitter} Status of restoration with `total` and `imported` + * @returns Status of restoration with `total` and `imported` * key counts. */ public restoreKeyBackupWithRecoveryKey( @@ -3435,8 +3583,8 @@ export class MatrixClient extends TypedEventEmitter { @@ -3468,7 +3616,7 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest( @@ -3483,8 +3631,8 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/account_data/$type", { @@ -3570,8 +3718,8 @@ export class MatrixClient extends TypedEventEmitter(eventType: string): Promise { if (this.isInitialSyncComplete()) { @@ -3611,7 +3759,7 @@ export class MatrixClient extends TypedEventEmitter { const content = { ignored_users: {} as Record }; @@ -3635,8 +3783,8 @@ export class MatrixClient extends TypedEventEmitterreturned Room object will have no current state. - * Default: true. - * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite, the signing URL is passed in this parameter. - * @param {string[]} opts.viaServers The server names to try and join through in addition to those that are automatically chosen. - * @return {Promise} Resolves: Room object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param roomIdOrAlias - The room ID or room alias to join. + * @param opts - Options when joining the room. + * @returns Promise which resolves: Room object. + * @returns Rejects: with an error response. */ public async joinRoom(roomIdOrAlias: string, opts: IJoinRoomOpts = {}): Promise { if (opts.syncRoom === undefined) { @@ -3702,11 +3845,11 @@ export class MatrixClient extends TypedEventEmitter { // also kick the to-device queue to retry @@ -3719,7 +3862,7 @@ export class MatrixClient extends TypedEventEmitter { return this.sendStateEvent(roomId, EventType.RoomName, { name: name }); } /** - * @param {string} roomId - * @param {string} topic - * @param {string} htmlTopic Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param htmlTopic - Optional. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ public setRoomTopic( roomId: string, @@ -3768,9 +3907,8 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags", { @@ -3781,11 +3919,10 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { @@ -3797,10 +3934,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { @@ -3812,11 +3948,10 @@ export class MatrixClient extends TypedEventEmitter { let cancelled = false; @@ -4161,9 +4276,9 @@ export class MatrixClient extends TypedEventEmitter { @@ -4792,11 +4872,8 @@ export class MatrixClient extends TypedEventEmitter { if (this.isGuest()) { @@ -4822,12 +4899,12 @@ export class MatrixClient extends TypedEventEmitter { return this.membershipChange(roomId, userId, "invite", reason); @@ -4907,10 +4982,10 @@ export class MatrixClient extends TypedEventEmitter { return this.inviteByThreePid(roomId, "email", email); @@ -4918,11 +4993,11 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri( @@ -4957,9 +5032,8 @@ export class MatrixClient extends TypedEventEmitter { return this.membershipChange(roomId, undefined, "leave"); @@ -4970,10 +5044,10 @@ export class MatrixClient extends TypedEventEmitter { return this.membershipChange(roomId, userId, "ban", reason); } /** - * @param {string} roomId - * @param {boolean} deleteRoom True to delete the room from the store on success. + * @param deleteRoom - True to delete the room from the store on success. * Default: true. - * @return {Promise} Resolves: {} an empty object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: `{}` an empty object. + * @returns Rejects: with an error response. */ public forget(roomId: string, deleteRoom = true): Promise<{}> { const promise = this.membershipChange(roomId, undefined, "forget"); @@ -5043,10 +5114,8 @@ export class MatrixClient extends TypedEventEmitter { // unbanning != set their state to leave: this used to be @@ -5064,11 +5133,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/kick", { @@ -5102,10 +5169,10 @@ export class MatrixClient extends TypedEventEmitter; @@ -5132,9 +5199,8 @@ export class MatrixClient extends TypedEventEmitter { const prom = await this.setProfileInfo("displayname", { displayname: name }); @@ -5148,9 +5214,8 @@ export class MatrixClient extends TypedEventEmitter { const prom = await this.setProfileInfo("avatar_url", { avatar_url: url }); @@ -5166,15 +5231,15 @@ export class MatrixClient extends TypedEventEmitterThis method is experimental and * may change. - * @param {string} mxcUrl The MXC URL - * @param {Number} width The desired width of the thumbnail. - * @param {Number} height The desired height of the thumbnail. - * @param {string} resizeMethod The thumbnail resize method to use, either + * @param mxcUrl - The MXC URL + * @param width - The desired width of the thumbnail. + * @param height - The desired height of the thumbnail. + * @param resizeMethod - The thumbnail resize method to use, either * "crop" or "scale". - * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs + * @param allowDirectLinks - If true, return any non-mxc URLs * directly. Fetching such URLs will leak information about the user to * anyone they share a room with. If false, will return null for such URLs. - * @return {?string} the avatar URL or null. + * @returns the avatar URL or null. */ public mxcUrlToHttp( mxcUrl: string, @@ -5187,11 +5252,9 @@ export class MatrixClient extends TypedEventEmitter { @@ -5207,9 +5270,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/presence/$userId/status", { @@ -5227,13 +5290,13 @@ export class MatrixClient extends TypedEventEmitterRoom.oldState.paginationToken will be - * null. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Room. If you are at the beginning + * of the timeline, `Room.oldState.paginationToken` will be + * `null`. + * @returns Rejects: with an error response. */ public scrollback(room: Room, limit = 30): Promise { let timeToWaitMs = 0; @@ -5302,13 +5365,6 @@ export class MatrixClient extends TypedEventEmitter> { // don't allow any timeline support unless it's been enabled. @@ -5563,10 +5619,10 @@ export class MatrixClient extends TypedEventEmitter> { // don't allow any timeline support unless it's been enabled. @@ -5628,12 +5684,9 @@ export class MatrixClient extends TypedEventEmitter { @@ -5997,9 +6042,9 @@ export class MatrixClient extends TypedEventEmitter { this.peekSync?.stopPeeking(); @@ -6019,16 +6064,10 @@ export class MatrixClient extends TypedEventEmitter { const writePromise = this.sendStateEvent(roomId, EventType.RoomGuestAccess, { @@ -6053,11 +6092,11 @@ export class MatrixClient extends TypedEventEmitter( endpoint: string, @@ -6276,9 +6315,9 @@ export class MatrixClient extends TypedEventEmitter | undefined { let promise: Promise | undefined; @@ -6388,20 +6427,15 @@ export class MatrixClient extends TypedEventEmitter { // TODO: support search groups @@ -6433,9 +6467,9 @@ export class MatrixClient extends TypedEventEmitter(searchResults: T): Promise { // TODO: we should implement a backoff (as per scrollback()) to deal more @@ -6469,10 +6503,8 @@ export class MatrixClient extends TypedEventEmitter(searchResults: T, response: ISearchResponse): T { @@ -6511,9 +6543,9 @@ export class MatrixClient extends TypedEventEmitter { // Guard against multiple calls whilst ongoing and multiple calls post success @@ -6539,9 +6571,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/filter", { @@ -6558,12 +6590,12 @@ export class MatrixClient extends TypedEventEmitter { if (allowCached) { @@ -6587,9 +6619,7 @@ export class MatrixClient extends TypedEventEmitter} Filter ID + * @returns Filter ID */ public async getOrCreateFilter(filterName: string, filter: Filter): Promise { const filterId = this.store.getFilterIdByName(filterName); @@ -6642,8 +6672,8 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/openid/request_token", { @@ -6662,8 +6692,8 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest(Method.Get, "/voip/turnServer"); @@ -6671,7 +6701,7 @@ export class MatrixClient extends TypedEventEmitter} The servers or an empty list. + * @returns The servers or an empty list. */ public getTurnServers(): ITurnServer[] { return this.turnServers || []; @@ -6680,7 +6710,7 @@ export class MatrixClient extends TypedEventEmitterThis function is implementation specific and may change * as a result. - * @return {boolean} true if the user appears to be a Synapse administrator. + * @returns true if the user appears to be a Synapse administrator. */ public isSynapseAdministrator(): Promise { const path = utils.encodeUri( @@ -6780,8 +6809,8 @@ export class MatrixClient extends TypedEventEmitterThis function is implementation specific and may change as a * result. - * @param {string} userId the User ID to look up. - * @return {object} the whois response - see Synapse docs for information. + * @param userId - the User ID to look up. + * @returns the whois response - see Synapse docs for information. */ public whoisSynapseUser(userId: string): Promise { const path = utils.encodeUri( @@ -6794,8 +6823,8 @@ export class MatrixClient extends TypedEventEmitterThis * function is implementation specific and may change as a result. - * @param {string} userId the User ID to deactivate. - * @return {object} the deactivate response - see Synapse docs for information. + * @param userId - the User ID to deactivate. + * @returns the deactivate response - see Synapse docs for information. */ public deactivateSynapseUser(userId: string): Promise { const path = utils.encodeUri( @@ -6828,8 +6857,8 @@ export class MatrixClient extends TypedEventEmitter { // XXX: Intended private, used in code const primTypes = ["boolean", "string", "number"]; @@ -6846,9 +6875,9 @@ export class MatrixClient extends TypedEventEmitter} Resolves to a set of rooms - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param userId - The userId to check. + * @returns Promise which resolves to a set of rooms + * @returns Rejects: with an error response. */ public async _unstable_getSharedRooms(userId: string): Promise { // eslint-disable-line const sharedRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666"); @@ -6873,7 +6902,7 @@ export class MatrixClient extends TypedEventEmitter} The server /versions response + * @returns The server /versions response */ public async getVersions(): Promise { if (this.serverVersionsPromise) { @@ -6906,8 +6935,8 @@ export class MatrixClient extends TypedEventEmitter} Whether it is supported + * @param version - The spec version (such as "r0.5.0") to check for. + * @returns Whether it is supported */ public async isVersionSupported(version: string): Promise { const { versions } = await this.getVersions(); @@ -6916,7 +6945,7 @@ export class MatrixClient extends TypedEventEmitter} true if server supports lazy loading + * @returns true if server supports lazy loading */ public async doesServerSupportLazyLoading(): Promise { const response = await this.getVersions(); @@ -6932,7 +6961,7 @@ export class MatrixClient extends TypedEventEmitter} true if id_server parameter is required + * @returns true if id_server parameter is required */ public async doesServerRequireIdServerParam(): Promise { const response = await this.getVersions(); @@ -6958,7 +6987,7 @@ export class MatrixClient extends TypedEventEmitter} true if id_access_token can be sent + * @returns true if id_access_token can be sent */ public async doesServerAcceptIdentityAccessToken(): Promise { const response = await this.getVersions(); @@ -6974,7 +7003,7 @@ export class MatrixClient extends TypedEventEmitter} true if separate functions are supported + * @returns true if separate functions are supported */ public async doesServerSupportSeparateAddAndBind(): Promise { const response = await this.getVersions(); @@ -6989,8 +7018,8 @@ export class MatrixClient extends TypedEventEmitter} true if the feature is supported + * @param feature - the feature name + * @returns true if the feature is supported */ public async doesServerSupportUnstableFeature(feature: string): Promise { const response = await this.getVersions(); @@ -7002,8 +7031,8 @@ export class MatrixClient extends TypedEventEmitter} true if the server is forcing encryption + * @param presetName - The name of the preset to check. + * @returns true if the server is forcing encryption * for the preset. */ public async doesServerForceEncryptionForPreset(presetName: Preset): Promise { @@ -7062,7 +7091,7 @@ export class MatrixClient extends TypedEventEmitter} true if server supports the `logout_devices` parameter + * @returns true if server supports the `logout_devices` parameter */ public doesServerSupportLogoutDevices(): Promise { return this.isVersionSupported("r0.6.1"); @@ -7070,7 +7099,7 @@ export class MatrixClient extends TypedEventEmitterThis * method is experimental and may change. - * @return {string} A new client secret + * @returns A new client secret */ public generateClientSecret(): string { return randomString(32); @@ -7170,11 +7198,8 @@ export class MatrixClient extends TypedEventEmitter} A decryption promise - * @param {object} options - * @param {boolean} options.isRetry True if this is a retry (enables more logging) - * @param {boolean} options.emit Emits "event.decrypted" if set to true + * @param event - The event to decrypt + * @returns A decryption promise */ public decryptEventIfNeeded(event: MatrixEvent, options?: IDecryptOptions): Promise { if (event.shouldAttemptDecryption() && this.isCryptoEnabled()) { @@ -7201,7 +7226,7 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest<{ available: true }>( @@ -7280,17 +7305,11 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types - opts = opts || {}; - opts.body = opts.body || {}; - return this.registerRequest(opts.body, "guest"); + public registerGuest({ body }: { body?: any } = {}): Promise { // TODO: Types + return this.registerRequest(body || {}, "guest"); } /** - * @param {Object} data parameters for registration request - * @param {string=} kind type of user to register. may be "guest" - * @return {Promise} Resolves: to the /register response - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param data - parameters for registration request + * @param kind - type of user to register. may be "guest" + * @returns Promise which resolves: to the /register response + * @returns Rejects: with an error response. */ public registerRequest(data: IRegisterRequestParams, kind?: string): Promise { const params: { kind?: string } = {}; @@ -7396,9 +7412,9 @@ export class MatrixClient extends TypedEventEmitter} Resolves to the new token. - * @return {module:http-api.MatrixError} Rejects with an error response. + * @param refreshToken - The refresh token. + * @returns Promise which resolves to the new token. + * @returns Rejects with an error response. */ public refreshToken(refreshToken: string): Promise { return this.http.authedRequest( @@ -7414,18 +7430,16 @@ export class MatrixClient extends TypedEventEmitter} Resolves to the available login flows - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves to the available login flows + * @returns Rejects: with an error response. */ public loginFlows(): Promise { return this.http.request(Method.Get, "/login"); } /** - * @param {string} loginType - * @param {Object} data - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ public login(loginType: string, data: any): Promise { // TODO: Types const loginData = { @@ -7450,10 +7464,8 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types return this.login("m.login.password", { @@ -7463,9 +7475,9 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types return this.login("m.login.saml2", { @@ -7474,22 +7486,22 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types return this.login("m.login.token", { @@ -7527,8 +7539,8 @@ export class MatrixClient extends TypedEventEmitter { if (this.crypto?.backupManager?.getKeyBackupEnabled()) { @@ -7556,11 +7568,11 @@ export class MatrixClient extends TypedEventEmitter { const body: any = {}; @@ -7579,8 +7591,8 @@ export class MatrixClient extends TypedEventEmitter>} Resolves: On success, the token response + * @param auth - Optional. Auth data to supply for User-Interactive auth. + * @returns Promise which resolves: On success, the token response * or UIA auth data. */ public requestLoginToken(auth?: IAuthData): Promise> { @@ -7597,10 +7609,10 @@ export class MatrixClient extends TypedEventEmitter{room_id: {string}} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param options - a list of options to pass to the /createRoom API. + * @returns Promise which resolves: `{room_id: {string}}` + * @returns Rejects: with an error response. */ public async createRoom(options: ICreateRoomOpts): Promise<{ room_id: string }> { // eslint-disable-line camelcase // some valid options include: room_alias_name, visibility, invite @@ -7648,12 +7654,12 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/state", { $roomId: roomId }); @@ -7706,11 +7711,9 @@ export class MatrixClient extends TypedEventEmitter> { const path = utils.encodeUri( @@ -7723,12 +7726,11 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/initialSync", @@ -7841,14 +7833,14 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/joined_rooms", {}); @@ -7886,10 +7878,10 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/joined_members", { @@ -7899,16 +7891,14 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/directory/room/$alias", { @@ -7941,9 +7931,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/directory/room/$alias", { @@ -7955,9 +7945,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId }); @@ -7967,9 +7957,9 @@ export class MatrixClient extends TypedEventEmitter { @@ -7995,9 +7984,8 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/directory/list/room/$roomId", { @@ -8008,12 +7996,11 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/directory/list/room/$roomId", { @@ -8025,14 +8012,13 @@ export class MatrixClient extends TypedEventEmitter { + public searchUserDirectory({ term, limit }: { term: string, limit?: number }): Promise { const body: any = { - search_term: opts.term, + search_term: term, }; - if (opts.limit !== undefined) { - body.limit = opts.limit; + if (limit !== undefined) { + body.limit = limit; } return this.http.authedRequest(Method.Post, "/user_directory/search", undefined, body); @@ -8069,27 +8054,13 @@ export class MatrixClient extends TypedEventEmitterfile.name. - * - * @param {boolean=} opts.includeFilename if false will not send the filename, - * e.g for encrypted file uploads where filename leaks are undesirable. - * Defaults to true. - * - * @param {string=} opts.type Content-type for the upload. Defaults to - * file.type, or applicaton/octet-stream. - * - * @param {Function=} opts.progressHandler Optional. Called when a chunk of - * data has been uploaded, with an object containing the fields `loaded` - * (number of bytes transferred) and `total` (total size, if known). + * @param opts - options object * - * @return {Promise} Resolves to response object, as + * @returns Promise which resolves to response object, as * determined by this.opts.onlyData, opts.rawResponse, and * opts.onlyContentUri. Rejects with an error (usually a MatrixError). */ @@ -8099,8 +8070,8 @@ export class MatrixClient extends TypedEventEmitter): boolean { return this.http.cancelUpload(upload); @@ -8108,7 +8079,7 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest(Method.Get, "/account/3pid"); @@ -8153,10 +8123,8 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types const path = "/account/3pid"; @@ -8174,10 +8142,10 @@ export class MatrixClient extends TypedEventEmitter/requestToken` on the homeserver. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ public async addThreePidOnly(data: IAddThreePidOnlyBody): Promise<{}> { const path = "/account/3pid/add"; @@ -8193,11 +8161,11 @@ export class MatrixClient extends TypedEventEmitter/requestToken` on the identity server. It should also * contain `id_server` and `id_access_token` fields as well. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ public async bindThreePid(data: IBindThreePidBody): Promise<{}> { const path = "/account/3pid/bind"; @@ -8210,11 +8178,11 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest(Method.Get, "/devices"); @@ -8282,9 +8249,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/devices/$device_id", { @@ -8296,10 +8263,10 @@ export class MatrixClient extends TypedEventEmitter { @@ -8313,10 +8280,10 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/devices/$device_id", { @@ -8335,10 +8302,10 @@ export class MatrixClient extends TypedEventEmitter { const body: any = { devices }; @@ -8354,8 +8321,8 @@ export class MatrixClient extends TypedEventEmitter { const response = await this.http.authedRequest<{ pushers: IPusher[] }>(Method.Get, "/pushers"); @@ -8377,9 +8344,9 @@ export class MatrixClient extends TypedEventEmitter { const path = "/pushers/set"; @@ -8388,10 +8355,8 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest(Method.Get, "/pushrules/").then((rules: IPushRules) => { @@ -8413,12 +8378,8 @@ export class MatrixClient extends TypedEventEmitter { const queryParams: any = {}; - if (opts.next_batch) { - queryParams.next_batch = opts.next_batch; + if (nextBatch) { + queryParams.next_batch = nextBatch; } - return this.http.authedRequest(Method.Post, "/search", queryParams, opts.body, { abortSignal }); + return this.http.authedRequest(Method.Post, "/search", queryParams, body, { abortSignal }); } /** * Upload keys * - * @param {Object} content body of upload request + * @param content - body of upload request * - * @param {Object=} opts this method no longer takes any opts, + * @param opts - this method no longer takes any opts, * used to take opts.device_id but this was not removed from the spec as a redundant parameter * - * @return {Promise} Resolves: result object. Rejects: with - * an error response ({@link module:http-api.MatrixError}). + * @returns Promise which resolves: result object. Rejects: with + * an error response ({@link MatrixError}). */ public uploadKeysRequest( content: IUploadKeysRequest, @@ -8548,22 +8497,20 @@ export class MatrixClient extends TypedEventEmitter { + public downloadKeysForUsers(userIds: string[], { token }: { token?: string } = {}): Promise { const content: IQueryKeysRequest = { device_keys: {}, }; - if ('token' in opts) { - content.token = opts.token; + if (token !== undefined) { + content.token = token; } userIds.forEach((u) => { content.device_keys[u] = []; @@ -8575,15 +8522,15 @@ export class MatrixClient extends TypedEventEmitter { const qps = { @@ -8641,15 +8586,14 @@ export class MatrixClient extends TypedEventEmitter} The hashing information for the identity server. + * @param identityAccessToken - The access token for the identity server. + * @returns The hashing information for the identity server. */ public getIdentityHashDetails(identityAccessToken: string): Promise { // TODO: Types return this.http.idServerRequest( @@ -8842,11 +8786,11 @@ export class MatrixClient extends TypedEventEmitter>} addressPairs An array of 2 element arrays. + * @param addressPairs - An array of 2 element arrays. * The first element of each pair is the address, the second is the 3PID medium. - * Eg: ["email@example.org", "email"] - * @param {string} identityAccessToken The access token for the identity server. - * @returns {Promise>} A collection of address mappings to + * Eg: `["email@example.org", "email"]` + * @param identityAccessToken - The access token for the identity server. + * @returns A collection of address mappings to * found MXIDs. Results where no user could be found will not be listed. */ public async identityHashedLookup( @@ -8925,15 +8869,15 @@ export class MatrixClient extends TypedEventEmitter>} query Array of arrays containing + * @param query - Array of arrays containing * [medium, address] - * @param {string} identityAccessToken The `access_token` field of the Identity + * @param identityAccessToken - The `access_token` field of the Identity * Server `/account/register` response (see {@link registerWithIdentityServer}). * - * @return {Promise} Resolves: Lookup results from IS. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Lookup results from IS. + * @returns Rejects: with an error response. */ public async bulkLookupThreePids(query: [string, string][], identityAccessToken: string): Promise { // TODO: Types // Note: we're using the V2 API by calling this function, but our @@ -9006,11 +8950,11 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types return this.http.idServerRequest( @@ -9025,12 +8969,11 @@ export class MatrixClient extends TypedEventEmitter>} contentMap + * @param eventType - type of event to send * content to send. Map from user_id to device_id to content object. - * @param {string=} txnId transaction id. One will be made up if not + * @param txnId - transaction id. One will be made up if not * supplied. - * @return {Promise} Resolves: to an empty object {} + * @returns Promise which resolves: to an empty object `{}` */ public sendToDevice( eventType: string, @@ -9060,7 +9003,7 @@ export class MatrixClient extends TypedEventEmitter { return this.toDeviceMessageQueue.queueBatch(batch); @@ -9069,7 +9012,7 @@ export class MatrixClient extends TypedEventEmitter { return this.http.authedRequest>( @@ -9086,10 +9029,10 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types const path = utils.encodeUri("/thirdparty/user/$protocol", { @@ -9140,11 +9083,11 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomId/report/$eventId", { @@ -9158,12 +9101,12 @@ export class MatrixClient extends TypedEventEmitter} Resolves to the created space. + * @param name - The name of the tree space. + * @returns Promise which resolves to the created space. */ public async unstableCreateFileTree(name: string): Promise { const { room_id: roomId } = await this.createRoom({ @@ -9244,8 +9187,8 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias }); @@ -9355,7 +9298,7 @@ export class MatrixClient extends TypedEventEmitter - * This is only fired for live events received via /sync - it is not fired for - * events received over context, search, or pagination APIs. - * - * @event module:client~MatrixClient#"event" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @example - * matrixClient.on("event", function(event){ - * var sender = event.getSender(); - * }); - */ - -/** - * Fires whenever the SDK receives a new to-device event. - * @event module:client~MatrixClient#"toDeviceEvent" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @example - * matrixClient.on("toDeviceEvent", function(event){ - * var sender = event.getSender(); - * }); - */ - -/** - * Fires whenever the SDK's syncing state is updated. The state can be one of: - *

    - * - *
  • PREPARED: The client has synced with the server at least once and is - * ready for methods to be called on it. This will be immediately followed by - * a state of SYNCING. This is the equivalent of "syncComplete" in the - * previous API.
  • - * - *
  • CATCHUP: The client has detected the connection to the server might be - * available again and will now try to do a sync again. As this sync might take - * a long time (depending how long ago was last synced, and general server - * performance) the client is put in this mode so the UI can reflect trying - * to catch up with the server after losing connection.
  • - * - *
  • SYNCING : The client is currently polling for new events from the server. - * This will be called after processing latest events from a sync.
  • - * - *
  • ERROR : The client has had a problem syncing with the server. If this is - * called before PREPARED then there was a problem performing the initial - * sync. If this is called after PREPARED then there was a problem polling - * the server for updates. This may be called multiple times even if the state is - * already ERROR. This is the equivalent of "syncError" in the previous - * API.
  • - * - *
  • RECONNECTING: The sync connection has dropped, but not (yet) in a way that - * should be considered erroneous. - *
  • - * - *
  • STOPPED: The client has stopped syncing with server due to stopClient - * being called. - *
  • - *
- * State transition diagram: - *
- *                                          +---->STOPPED
- *                                          |
- *              +----->PREPARED -------> SYNCING <--+
- *              |                        ^  |  ^    |
- *              |      CATCHUP ----------+  |  |    |
- *              |        ^                  V  |    |
- *   null ------+        |  +------- RECONNECTING   |
- *              |        V  V                       |
- *              +------->ERROR ---------------------+
- *
- * NB: 'null' will never be emitted by this event.
- *
- * 
- * Transitions: - *
    - * - *
  • null -> PREPARED : Occurs when the initial sync is completed - * first time. This involves setting up filters and obtaining push rules. - * - *
  • null -> ERROR : Occurs when the initial sync failed first time. - * - *
  • ERROR -> PREPARED : Occurs when the initial sync succeeds - * after previously failing. - * - *
  • PREPARED -> SYNCING : Occurs immediately after transitioning - * to PREPARED. Starts listening for live updates rather than catching up. - * - *
  • SYNCING -> RECONNECTING : Occurs when the live update fails. - * - *
  • RECONNECTING -> RECONNECTING : Can occur if the update calls - * continue to fail, but the keepalive calls (to /versions) succeed. - * - *
  • RECONNECTING -> ERROR : Occurs when the keepalive call also fails - * - *
  • ERROR -> SYNCING : Occurs when the client has performed a - * live update after having previously failed. - * - *
  • ERROR -> ERROR : Occurs when the client has failed to keepalive - * for a second time or more.
  • - * - *
  • SYNCING -> SYNCING : Occurs when the client has performed a live - * update. This is called after processing.
  • - * - *
  • * -> STOPPED : Occurs once the client has stopped syncing or - * trying to sync after stopClient has been called.
  • - *
- * - * @event module:client~MatrixClient#"sync" - * - * @param {string} state An enum representing the syncing state. One of "PREPARED", - * "SYNCING", "ERROR", "STOPPED". - * - * @param {?string} prevState An enum representing the previous syncing state. - * One of "PREPARED", "SYNCING", "ERROR", "STOPPED" or null. - * - * @param {?Object} data Data about this transition. - * - * @param {MatrixError} data.error The matrix error if state=ERROR. - * - * @param {String} data.oldSyncToken The 'since' token passed to /sync. - * null for the first successful sync since this client was - * started. Only present if state=PREPARED or - * state=SYNCING. - * - * @param {String} data.nextSyncToken The 'next_batch' result from /sync, which - * will become the 'since' token for the next call to /sync. Only present if - * state=PREPARED or state=SYNCING. - * - * @param {boolean} data.catchingUp True if we are working our way through a - * backlog of events after connecting. Only present if state=SYNCING. - * - * @example - * matrixClient.on("sync", function(state, prevState, data) { - * switch (state) { - * case "ERROR": - * // update UI to say "Connection Lost" - * break; - * case "SYNCING": - * // update UI to remove any "Connection Lost" message - * break; - * case "PREPARED": - * // the client instance is ready to be queried. - * var rooms = matrixClient.getRooms(); - * break; - * } - * }); - */ - -/** - * Fires whenever a new Room is added. This will fire when you are invited to a - * room, as well as when you join a room. This event is experimental and - * may change. - * @event module:client~MatrixClient#"Room" - * @param {Room} room The newly created, fully populated room. - * @example - * matrixClient.on("Room", function(room){ - * var roomId = room.roomId; - * }); - */ - -/** - * Fires whenever a Room is removed. This will fire when you forget a room. - * This event is experimental and may change. - * @event module:client~MatrixClient#"deleteRoom" - * @param {string} roomId The deleted room ID. - * @example - * matrixClient.on("deleteRoom", function(roomId){ - * // update UI from getRooms() - * }); - */ - -/** - * Fires whenever an incoming call arrives. - * @event module:client~MatrixClient#"Call.incoming" - * @param {module:webrtc/call~MatrixCall} call The incoming call. - * @example - * matrixClient.on("Call.incoming", function(call){ - * call.answer(); // auto-answer - * }); - */ - -/** - * Fires whenever the login session the JS SDK is using is no - * longer valid and the user must log in again. - * NB. This only fires when action is required from the user, not - * when then login session can be renewed by using a refresh token. - * @event module:client~MatrixClient#"Session.logged_out" - * @example - * matrixClient.on("Session.logged_out", function(errorObj){ - * // show the login screen - * }); - */ - -/** - * Fires when the JS SDK receives a M_CONSENT_NOT_GIVEN error in response - * to a HTTP request. - * @event module:client~MatrixClient#"no_consent" - * @example - * matrixClient.on("no_consent", function(message, contentUri) { - * console.info(message + ' Go to ' + contentUri); - * }); - */ - -/** - * Fires when a device is marked as verified/unverified/blocked/unblocked by - * {@link module:client~MatrixClient#setDeviceVerified|MatrixClient.setDeviceVerified} or - * {@link module:client~MatrixClient#setDeviceBlocked|MatrixClient.setDeviceBlocked}. - * - * @event module:client~MatrixClient#"deviceVerificationChanged" - * @param {string} userId the owner of the verified device - * @param {string} deviceId the id of the verified device - * @param {module:crypto/deviceinfo} deviceInfo updated device information - */ - -/** - * Fires when the trust status of a user changes - * If userId is the userId of the logged in user, this indicated a change - * in the trust status of the cross-signing data on the account. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @event module:client~MatrixClient#"userTrustStatusChanged" - * @param {string} userId the userId of the user in question - * @param {UserTrustLevel} trustLevel The new trust level of the user - */ - -/** - * Fires when the user's cross-signing keys have changed or cross-signing - * has been enabled/disabled. The client can use getStoredCrossSigningForUser - * with the user ID of the logged in user to check if cross-signing is - * enabled on the account. If enabled, it can test whether the current key - * is trusted using with checkUserTrust with the user ID of the logged - * in user. The checkOwnCrossSigningTrust function may be used to reconcile - * the trust in the account key. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @event module:client~MatrixClient#"crossSigning.keysChanged" - */ - -/** - * Fires whenever new user-scoped account_data is added. - * @event module:client~MatrixClient#"accountData" - * @param {MatrixEvent} event The event describing the account_data just added - * @param {MatrixEvent} event The previous account data, if known. - * @example - * matrixClient.on("accountData", function(event, oldEvent){ - * myAccountData[event.type] = event.content; - * }); - */ - -/** - * Fires whenever the stored devices for a user have changed - * @event module:client~MatrixClient#"crypto.devicesUpdated" - * @param {String[]} users A list of user IDs that were updated - * @param {boolean} initialFetch If true, the store was empty (apart - * from our own device) and has been seeded. - */ - -/** - * Fires whenever the stored devices for a user will be updated - * @event module:client~MatrixClient#"crypto.willUpdateDevices" - * @param {String[]} users A list of user IDs that will be updated - * @param {boolean} initialFetch If true, the store is empty (apart - * from our own device) and is being seeded. - */ - -/** - * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled() - * @event module:client~MatrixClient#"crypto.keyBackupStatus" - * @param {boolean} enabled true if key backup has been enabled, otherwise false - * @example - * matrixClient.on("crypto.keyBackupStatus", function(enabled){ - * if (enabled) { - * [...] - * } - * }); - */ - -/** - * Fires when we want to suggest to the user that they restore their megolm keys - * from backup or by cross-signing the device. - * - * @event module:client~MatrixClient#"crypto.suggestKeyRestore" - */ - -/** - * Fires when a key verification is requested. - * @event module:client~MatrixClient#"crypto.verification.request" - * @param {object} data - * @param {MatrixEvent} data.event the original verification request message - * @param {Array} data.methods the verification methods that can be used - * @param {Number} data.timeout the amount of milliseconds that should be waited - * before cancelling the request automatically. - * @param {Function} data.beginKeyVerification a function to call if a key - * verification should be performed. The function takes one argument: the - * name of the key verification method (taken from data.methods) to use. - * @param {Function} data.cancel a function to call if the key verification is - * rejected. - */ - -/** - * Fires when a key verification is requested with an unknown method. - * @event module:client~MatrixClient#"crypto.verification.request.unknown" - * @param {string} userId the user ID who requested the key verification - * @param {Function} cancel a function that will send a cancellation message to - * reject the key verification. - */ - -/** - * Fires when a secret request has been cancelled. If the client is prompting - * the user to ask whether they want to share a secret, the prompt can be - * dismissed. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @event module:client~MatrixClient#"crypto.secrets.requestCancelled" - * @param {object} data - * @param {string} data.user_id The user ID of the client that had requested the secret. - * @param {string} data.device_id The device ID of the client that had requested the - * secret. - * @param {string} data.request_id The ID of the original request. - */ - -/** - * Fires when the client .well-known info is fetched. - * - * @event module:client~MatrixClient#"WellKnown.client" - * @param {object} data The JSON object returned by the server - */ diff --git a/src/content-helpers.ts b/src/content-helpers.ts index 6fa4b684b5b..b66f0d10235 100644 --- a/src/content-helpers.ts +++ b/src/content-helpers.ts @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** @module ContentHelpers */ - import { isProvided, REFERENCE_RELATION } from "matrix-events-sdk"; import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon"; @@ -37,9 +35,9 @@ import { IContent } from "./models/event"; /** * Generates the content for a HTML Message event - * @param {string} body the plaintext body of the message - * @param {string} htmlBody the HTML representation of the message - * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + * @param body - the plaintext body of the message + * @param htmlBody - the HTML representation of the message + * @returns */ export function makeHtmlMessage(body: string, htmlBody: string): IContent { return { @@ -52,9 +50,9 @@ export function makeHtmlMessage(body: string, htmlBody: string): IContent { /** * Generates the content for a HTML Notice event - * @param {string} body the plaintext body of the notice - * @param {string} htmlBody the HTML representation of the notice - * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + * @param body - the plaintext body of the notice + * @param htmlBody - the HTML representation of the notice + * @returns */ export function makeHtmlNotice(body: string, htmlBody: string): IContent { return { @@ -67,9 +65,9 @@ export function makeHtmlNotice(body: string, htmlBody: string): IContent { /** * Generates the content for a HTML Emote event - * @param {string} body the plaintext body of the emote - * @param {string} htmlBody the HTML representation of the emote - * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + * @param body - the plaintext body of the emote + * @param htmlBody - the HTML representation of the emote + * @returns */ export function makeHtmlEmote(body: string, htmlBody: string): IContent { return { @@ -82,8 +80,8 @@ export function makeHtmlEmote(body: string, htmlBody: string): IContent { /** * Generates the content for a Plaintext Message event - * @param {string} body the plaintext body of the emote - * @returns {{msgtype: string, body: string}} + * @param body - the plaintext body of the emote + * @returns */ export function makeTextMessage(body: string): IContent { return { @@ -94,8 +92,8 @@ export function makeTextMessage(body: string): IContent { /** * Generates the content for a Plaintext Notice event - * @param {string} body the plaintext body of the notice - * @returns {{msgtype: string, body: string}} + * @param body - the plaintext body of the notice + * @returns */ export function makeNotice(body: string): IContent { return { @@ -106,8 +104,8 @@ export function makeNotice(body: string): IContent { /** * Generates the content for a Plaintext Emote event - * @param {string} body the plaintext body of the emote - * @returns {{msgtype: string, body: string}} + * @param body - the plaintext body of the emote + * @returns */ export function makeEmoteMessage(body: string): IContent { return { @@ -139,11 +137,11 @@ export const getTextForLocationEvent = ( /** * Generates the content for a Location event - * @param uri a geo:// uri for the location - * @param timestamp the timestamp when the location was correct (milliseconds since the UNIX epoch) - * @param description the (optional) label for this location on the map - * @param assetType the (optional) asset type of this location e.g. "m.self" - * @param text optional. A text for the location + * @param uri - a geo:// uri for the location + * @param timestamp - the timestamp when the location was correct (milliseconds since the UNIX epoch) + * @param description - the (optional) label for this location on the map + * @param assetType - the (optional) asset type of this location e.g. "m.self" + * @param text - optional. A text for the location */ export const makeLocationContent = ( // this is first but optional diff --git a/src/content-repo.ts b/src/content-repo.ts index d6cf81f2ec6..9b6f6d37217 100644 --- a/src/content-repo.ts +++ b/src/content-repo.ts @@ -13,25 +13,22 @@ 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. */ -/** - * @module content-repo - */ import * as utils from "./utils"; /** * Get the HTTP URL for an MXC URI. - * @param {string} baseUrl The base homeserver url which has a content repo. - * @param {string} mxc The mxc:// URI. - * @param {Number} width The desired width of the thumbnail. - * @param {Number} height The desired height of the thumbnail. - * @param {string} resizeMethod The thumbnail resize method to use, either + * @param baseUrl - The base homeserver url which has a content repo. + * @param mxc - The mxc:// URI. + * @param width - The desired width of the thumbnail. + * @param height - The desired height of the thumbnail. + * @param resizeMethod - The thumbnail resize method to use, either * "crop" or "scale". - * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs + * @param allowDirectLinks - If true, return any non-mxc URLs * directly. Fetching such URLs will leak information about the user to * anyone they share a room with. If false, will return the emptry string * for such URLs. - * @return {string} The complete URL to the content. + * @returns The complete URL to the content. */ export function getHttpUriForMxc( baseUrl: string, diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index 4035db04cf7..1ac9e214479 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -16,7 +16,6 @@ limitations under the License. /** * Cross signing methods - * @module crypto/CrossSigning */ import { PkSigning } from "@matrix-org/olm"; @@ -67,12 +66,10 @@ export class CrossSigningInfo { /** * Information about a user's cross-signing keys * - * @class - * - * @param {string} userId the user that the information is about - * @param {object} callbacks Callbacks used to interact with the app + * @param userId - the user that the information is about + * @param callbacks - Callbacks used to interact with the app * Requires getCrossSigningKey and saveCrossSigningKeys - * @param {object} cacheCallbacks Callbacks used to interact with the cache + * @param cacheCallbacks - Callbacks used to interact with the cache */ public constructor( public readonly userId: string, @@ -102,10 +99,10 @@ export class CrossSigningInfo { /** * Calls the app callback to ask for a private key * - * @param {string} type The key type ("master", "self_signing", or "user_signing") - * @param {string} expectedPubkey The matching public key or undefined to use + * @param type - The key type ("master", "self_signing", or "user_signing") + * @param expectedPubkey - The matching public key or undefined to use * the stored public key for the given key type. - * @returns {Array} An array with [ public key, Olm.PkSigning ] + * @returns An array with [ public key, Olm.PkSigning ] */ public async getCrossSigningKey(type: string, expectedPubkey?: string): Promise<[string, PkSigning]> { const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0; @@ -165,8 +162,8 @@ export class CrossSigningInfo { * XXX: This could be static, be we often seem to have an instance when we * want to know this anyway... * - * @param {SecretStorage} secretStorage The secret store using account data - * @returns {object} map of key name to key info the secret is encrypted + * @param secretStorage - The secret store using account data + * @returns map of key name to key info the secret is encrypted * with, or null if it is not present or not encrypted with a trusted * key */ @@ -194,8 +191,8 @@ export class CrossSigningInfo { * typically called in conjunction with the creation of new cross-signing * keys. * - * @param {Map} keys The keys to store - * @param {SecretStorage} secretStorage The secret store using account data + * @param keys - The keys to store + * @param secretStorage - The secret store using account data */ public static async storeInSecretStorage( keys: Map, @@ -211,10 +208,10 @@ export class CrossSigningInfo { * Get private keys from secret storage created by some other device. This * also passes the private keys to the app-specific callback. * - * @param {string} type The type of key to get. One of "master", + * @param type - The type of key to get. One of "master", * "self_signing", or "user_signing". - * @param {SecretStorage} secretStorage The secret store using account data - * @return {Uint8Array} The private key + * @param secretStorage - The secret store using account data + * @returns The private key */ public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise { const encodedKey = await secretStorage.get(`m.cross_signing.${type}`); @@ -227,9 +224,9 @@ export class CrossSigningInfo { /** * Check whether the private keys exist in the local key cache. * - * @param {string} [type] The type of key to get. One of "master", + * @param type - The type of key to get. One of "master", * "self_signing", or "user_signing". Optional, will check all by default. - * @returns {boolean} True if all keys are stored in the local cache. + * @returns True if all keys are stored in the local cache. */ public async isStoredInKeyCache(type?: string): Promise { const cacheCallbacks = this.cacheCallbacks; @@ -246,7 +243,7 @@ export class CrossSigningInfo { /** * Get cross-signing private keys from the local cache. * - * @returns {Map} A map from key type (string) to private key (Uint8Array) + * @returns A map from key type (string) to private key (Uint8Array) */ public async getCrossSigningKeysFromCache(): Promise> { const keys = new Map(); @@ -266,10 +263,10 @@ export class CrossSigningInfo { * Get the ID used to identify the user. This can also be used to test for * the existence of a given key type. * - * @param {string} type The type of key to get the ID of. One of "master", + * @param type - The type of key to get the ID of. One of "master", * "self_signing", or "user_signing". Defaults to "master". * - * @return {string} the ID + * @returns the ID */ public getId(type = "master"): string | null { if (!this.keys[type]) return null; @@ -282,7 +279,7 @@ export class CrossSigningInfo { * will be held in this class, while the private keys are passed off to the * `saveCrossSigningKeys` application callback. * - * @param {CrossSigningLevel} level The key types to reset + * @param level - The key types to reset */ public async resetKeys(level?: CrossSigningLevel): Promise { if (!this.callbacks.saveCrossSigningKeys) { @@ -502,9 +499,9 @@ export class CrossSigningInfo { /** * Check whether a given user is trusted. * - * @param {CrossSigningInfo} userCrossSigning Cross signing info for user + * @param userCrossSigning - Cross signing info for user * - * @returns {UserTrustLevel} + * @returns */ public checkUserTrust(userCrossSigning: CrossSigningInfo): UserTrustLevel { // if we're checking our own key, then it's trusted if the master key @@ -542,12 +539,12 @@ export class CrossSigningInfo { /** * Check whether a given device is trusted. * - * @param {CrossSigningInfo} userCrossSigning Cross signing info for user - * @param {module:crypto/deviceinfo} device The device to check - * @param {boolean} localTrust Whether the device is trusted locally - * @param {boolean} trustCrossSignedDevices Whether we trust cross signed devices + * @param userCrossSigning - Cross signing info for user + * @param device - The device to check + * @param localTrust - Whether the device is trusted locally + * @param trustCrossSignedDevices - Whether we trust cross signed devices * - * @returns {DeviceTrustLevel} + * @returns */ public checkDeviceTrust( userCrossSigning: CrossSigningInfo, @@ -580,7 +577,7 @@ export class CrossSigningInfo { } /** - * @returns {object} Cache callbacks + * @returns Cache callbacks */ public getCacheCallbacks(): ICacheCallbacks { return this.cacheCallbacks; @@ -621,21 +618,21 @@ export class UserTrustLevel { ) {} /** - * @returns {boolean} true if this user is verified via any means + * @returns true if this user is verified via any means */ public isVerified(): boolean { return this.isCrossSigningVerified(); } /** - * @returns {boolean} true if this user is verified via cross signing + * @returns true if this user is verified via cross signing */ public isCrossSigningVerified(): boolean { return this.crossSigningVerified; } /** - * @returns {boolean} true if we ever verified this user before (at least for + * @returns true if we ever verified this user before (at least for * the history of verifications observed by this device). */ public wasCrossSigningVerified(): boolean { @@ -643,7 +640,7 @@ export class UserTrustLevel { } /** - * @returns {boolean} true if this user's key is trusted on first use + * @returns true if this user's key is trusted on first use */ public isTofu(): boolean { return this.tofu; @@ -675,7 +672,7 @@ export class DeviceTrustLevel { } /** - * @returns {boolean} true if this device is verified via any means + * @returns true if this device is verified via any means */ public isVerified(): boolean { return Boolean(this.isLocallyVerified() || ( @@ -684,21 +681,21 @@ export class DeviceTrustLevel { } /** - * @returns {boolean} true if this device is verified via cross signing + * @returns true if this device is verified via cross signing */ public isCrossSigningVerified(): boolean { return this.crossSigningVerified; } /** - * @returns {boolean} true if this device is verified locally + * @returns true if this device is verified locally */ public isLocallyVerified(): boolean { return this.localVerified; } /** - * @returns {boolean} true if this device is trusted from a user's key + * @returns true if this device is trusted from a user's key * that is trusted on first use */ public isTofu(): boolean { @@ -757,9 +754,9 @@ export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning], /** * Request cross-signing keys from another device during verification. * - * @param {MatrixClient} baseApis base Matrix API interface - * @param {string} userId The user ID being verified - * @param {string} deviceId The device ID being verified + * @param baseApis - base Matrix API interface + * @param userId - The user ID being verified + * @param deviceId - The device ID being verified */ export async function requestKeysDuringVerification( baseApis: MatrixClient, diff --git a/src/crypto/DeviceList.ts b/src/crypto/DeviceList.ts index da40a03f231..81dc5e18e68 100644 --- a/src/crypto/DeviceList.ts +++ b/src/crypto/DeviceList.ts @@ -15,8 +15,6 @@ limitations under the License. */ /** - * @module crypto/DeviceList - * * Manages the list of other users' devices */ @@ -64,9 +62,6 @@ export type DeviceInfoMap = Record>; type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated; -/** - * @alias module:crypto/DeviceList - */ export class DeviceList extends TypedEventEmitter { private devices: { [userId: string]: { [deviceId: string]: IDevice } } = {}; @@ -163,11 +158,11 @@ export class DeviceList extends TypedEventEmitter} true if the data was saved, false if + * @returns true if the data was saved, false if * it was not (eg. because no changes were pending). The promise * will only resolve once the data is saved, so may take some time * to resolve. @@ -236,7 +231,7 @@ export class DeviceList extends TypedEventEmitterdeviceId->{@link - * module:crypto/deviceinfo|DeviceInfo}. + * @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo}. */ public downloadKeys(userIds: string[], forceDownload: boolean): Promise { const usersToDownload: string[] = []; @@ -303,9 +297,9 @@ export class DeviceList extends TypedEventEmitterdeviceId->{@link module:crypto/deviceinfo|DeviceInfo}. + * @returns userId-\>deviceId-\>{@link DeviceInfo}. */ private getDevicesFromStore(userIds: string[]): DeviceInfoMap { const stored: DeviceInfoMap = {}; @@ -322,7 +316,7 @@ export class DeviceList extends TypedEventEmitter{object} devices, or undefined if + * @returns `deviceId->{object}` devices, or undefined if * there is no data for this user. */ public getRawStoredDevicesForUser(userId: string): Record { @@ -376,10 +370,8 @@ export class DeviceList extends TypedEventEmitter): void { this.setRawStoredDevicesForUser(userId, devices); @@ -468,7 +458,6 @@ export class DeviceList extends TypedEventEmitter { @@ -569,9 +556,9 @@ export class DeviceList extends TypedEventEmitter{object} the new devices + * @param devices - `deviceId->{object}` the new devices */ public setRawStoredDevicesForUser(userId: string, devices: Record): void { // remove old devices from userByIdentityKey @@ -602,9 +589,9 @@ export class DeviceList extends TypedEventEmitter} accountData pre-existing account data, will only be read, not written. - * @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet + * @param accountData - pre-existing account data, will only be read, not written. + * @param delegateCryptoCallbacks - crypto callbacks to delegate to if the key isn't in cache yet */ public constructor(accountData: Record, delegateCryptoCallbacks?: ICryptoCallbacks) { this.accountDataClientAdapter = new AccountDataClientAdapter(accountData); @@ -70,13 +70,13 @@ export class EncryptionSetupBuilder { /** * Adds new cross-signing public keys * - * @param {function} authUpload Function called to await an interactive auth + * @param authUpload - Function called to await an interactive auth * flow when uploading device signing keys. * Args: - * {function} A function that makes the request requiring auth. Receives + * A function that makes the request requiring auth. Receives * the auth data as an object. Can be called multiple times, first with * an empty authDict, to obtain the flows. - * @param {Object} keys the new keys + * @param keys - the new keys */ public addCrossSigningKeys(authUpload: ICrossSigningKeys["authUpload"], keys: ICrossSigningKeys["keys"]): void { this.crossSigningKeys = { authUpload, keys }; @@ -88,7 +88,7 @@ export class EncryptionSetupBuilder { * Used either to create a new key backup, or add signatures * from the new MSK. * - * @param {Object} keyBackupInfo as received from/sent to the server + * @param keyBackupInfo - as received from/sent to the server */ public addSessionBackup(keyBackupInfo: IKeyBackupInfo): void { this.keyBackupInfo = keyBackupInfo; @@ -99,7 +99,6 @@ export class EncryptionSetupBuilder { * * Used after fixing the format of the key * - * @param {Uint8Array} privateKey */ public addSessionBackupPrivateKeyToCache(privateKey: Uint8Array): void { this.sessionBackupPrivateKey = privateKey; @@ -109,9 +108,6 @@ export class EncryptionSetupBuilder { * Add signatures from a given user and device/x-sign key * Used to sign the new cross-signing key with the device key * - * @param {String} userId - * @param {String} deviceId - * @param {Object} signature */ public addKeySignature(userId: string, deviceId: string, signature: ISignedKey): void { if (!this.keySignatures) { @@ -122,18 +118,12 @@ export class EncryptionSetupBuilder { userSignatures[deviceId] = signature; } - /** - * @param {String} type - * @param {Object} content - * @return {Promise} - */ public async setAccountData(type: string, content: object): Promise { await this.accountDataClientAdapter.setAccountData(type, content); } /** * builds the operation containing all the parts that have been added to the builder - * @return {EncryptionSetupOperation} */ public buildOperation(): EncryptionSetupOperation { const accountData = this.accountDataClientAdapter.values; @@ -150,9 +140,6 @@ export class EncryptionSetupBuilder { * * This does not yet store the operation in a way that it can be restored, * but that is the idea in the future. - * - * @param {Crypto} crypto - * @return {Promise} */ public async persist(crypto: Crypto): Promise { // store private keys in cache @@ -187,10 +174,6 @@ export class EncryptionSetupBuilder { */ export class EncryptionSetupOperation { /** - * @param {Map} accountData - * @param {Object} crossSigningKeys - * @param {Object} keyBackupInfo - * @param {Object} keySignatures */ public constructor( private readonly accountData: Map, @@ -201,7 +184,6 @@ export class EncryptionSetupOperation { /** * Runs the (remaining part of, in the future) operation by sending requests to the server. - * @param {Crypto} crypto */ public async apply(crypto: Crypto): Promise { const baseApis = crypto.baseApis; @@ -270,23 +252,21 @@ class AccountDataClientAdapter public readonly values = new Map(); /** - * @param {Object.} existingValues existing account data + * @param existingValues - existing account data */ public constructor(private readonly existingValues: Record) { super(); } /** - * @param {String} type - * @return {Promise} the content of the account data + * @returns the content of the account data */ public getAccountDataFromServer(type: string): Promise { return Promise.resolve(this.getAccountData(type) as T); } /** - * @param {String} type - * @return {Object} the content of the account data + * @returns the content of the account data */ public getAccountData(type: string): IContent | null { const modifiedValue = this.values.get(type); @@ -300,11 +280,6 @@ class AccountDataClientAdapter return null; } - /** - * @param {String} type - * @param {Object} content - * @return {Promise} - */ public setAccountData(type: string, content: any): Promise<{}> { const lastEvent = this.values.get(type); this.values.set(type, content); diff --git a/src/crypto/OlmDevice.ts b/src/crypto/OlmDevice.ts index 13463ef115c..d7d62099d82 100644 --- a/src/crypto/OlmDevice.ts +++ b/src/crypto/OlmDevice.ts @@ -53,43 +53,22 @@ function checkPayloadLength(payloadString: string): void { } } -/** - * The type of object we use for importing and exporting megolm session data. - * - * @typedef {Object} module:crypto/OlmDevice.MegolmSessionData - * @property {String} sender_key Sender's Curve25519 device key - * @property {String[]} forwarding_curve25519_key_chain Devices which forwarded - * this session to us (normally empty). - * @property {Object} sender_claimed_keys Other keys the sender claims. - * @property {String} room_id Room this session is used in - * @property {String} session_id Unique id for the session - * @property {String} session_key Base64'ed key data - */ - interface IInitOpts { fromExportedDevice?: IExportedDevice; pickleKey?: string; } -/** - * data stored in the session store about an inbound group session - * - * @typedef {Object} InboundGroupSessionData - * @property {string} room_id - * @property {string} session pickled Olm.InboundGroupSession - * @property {Object} keysClaimed - * @property {Array} forwardingCurve25519KeyChain Devices involved in forwarding - * this session to us (normally empty). - * @property {boolean=} untrusted whether this session is untrusted. - * @property {boolean=} sharedHistory whether this session exists during the room being set to shared history. - */ - +/** data stored in the session store about an inbound group session */ export interface InboundGroupSessionData { room_id: string; // eslint-disable-line camelcase + /** pickled Olm.InboundGroupSession */ session: string; keysClaimed: Record; + /** Devices involved in forwarding this session to us (normally empty). */ forwardingCurve25519KeyChain: string[]; + /** whether this session is untrusted. */ untrusted?: boolean; + /** whether this session exists during the room being set to shared history. */ sharedHistory?: boolean; } @@ -134,20 +113,13 @@ type OneTimeKeys = { curve25519: { [keyId: string]: string } }; * OlmAccount and a number of OlmSessions. * * Accounts and sessions are kept pickled in the cryptoStore. - * - * @constructor - * @alias module:crypto/OlmDevice - * - * @param {Object} cryptoStore A store for crypto data - * - * @property {string} deviceCurve25519Key Curve25519 key for the account - * @property {string} deviceEd25519Key Ed25519 key for the account */ export class OlmDevice { public pickleKey = "DEFAULT_KEY"; // set by consumers - // don't know these until we load the account from storage in init() + /** Curve25519 key for the account, unknown until we load the account from storage in init() */ public deviceCurve25519Key: string | null = null; + /** Ed25519 key for the account, unknown until we load the account from storage in init() */ public deviceEd25519Key: string | null = null; private maxOneTimeKeys: number | null = null; @@ -184,7 +156,7 @@ export class OlmDevice { } /** - * @return {array} The version of Olm. + * @returns The version of Olm. */ public static getOlmVersion(): [number, number, number] { return global.Olm.get_library_version(); @@ -202,12 +174,11 @@ export class OlmDevice { * * Reads the device keys from the OlmAccount object. * - * @param {object} opts - * @param {object} opts.fromExportedDevice (Optional) data from exported device + * @param fromExportedDevice - (Optional) data from exported device * that must be re-created. * If present, opts.pickleKey is ignored * (exported data already provides a pickle key) - * @param {object} opts.pickleKey (Optional) pickle key to set instead of default one + * @param pickleKey - (Optional) pickle key to set instead of default one */ public async init({ pickleKey, fromExportedDevice }: IInitOpts = {}): Promise { let e2eKeys; @@ -245,9 +216,9 @@ export class OlmDevice { * Note that for now only the “account” and “sessions” stores are populated; * Other stores will be as with a new device. * - * @param {IExportedDevice} exportedData Data exported from another device + * @param exportedData - Data exported from another device * through the “export” method. - * @param {Olm.Account} account an olm account to initialize + * @param account - an olm account to initialize */ private async initialiseFromExportedDevice(exportedData: IExportedDevice, account: Account): Promise { await this.cryptoStore.doTxn( @@ -305,9 +276,8 @@ export class OlmDevice { * This function requires a live transaction object from cryptoStore.doTxn() * and therefore may only be called in a doTxn() callback. * - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @param {function} func - * @private + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @internal */ private getAccount(txn: unknown, func: (account: Account) => void): void { this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => { @@ -326,9 +296,9 @@ export class OlmDevice { * This function requires a live transaction object from cryptoStore.doTxn() * and therefore may only be called in a doTxn() callback. * - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @param {object} Olm.Account object - * @private + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @param Olm.Account object + * @internal */ private storeAccount(txn: unknown, account: Account): void { this.cryptoStore.storeAccount(txn, account.pickle(this.pickleKey)); @@ -338,7 +308,7 @@ export class OlmDevice { * Export data for re-creating the Olm device later. * TODO export data other than just account and (P2P) sessions. * - * @return {Promise} The exported data + * @returns The exported data */ public async export(): Promise { const result: Partial = { @@ -373,11 +343,8 @@ export class OlmDevice { * function and will be freed as soon the callback returns. It is *not* * usable for the rest of the lifetime of the transaction. * - * @param {string} deviceKey - * @param {string} sessionId - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @param {function} func - * @private + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @internal */ private getSession( deviceKey: string, @@ -397,9 +364,7 @@ export class OlmDevice { * function with it. The session object is destroyed once the function * returns. * - * @param {object} sessionInfo - * @param {function} func - * @private + * @internal */ private unpickleSession( sessionInfo: ISessionInfo, @@ -419,10 +384,9 @@ export class OlmDevice { /** * store our OlmSession in the session store * - * @param {string} deviceKey - * @param {object} sessionInfo {session: OlmSession, lastReceivedMessageTs: int} - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @private + * @param sessionInfo - `{session: OlmSession, lastReceivedMessageTs: int}` + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @internal */ private saveSession(deviceKey: string, sessionInfo: IUnpickledSessionInfo, txn: unknown): void { const sessionId = sessionInfo.session.session_id(); @@ -435,9 +399,8 @@ export class OlmDevice { /** * get an OlmUtility and call the given function * - * @param {function} func - * @return {object} result of func - * @private + * @returns result of func + * @internal */ private getUtility(func: (utility: Utility) => T): T { const utility = new global.Olm.Utility(); @@ -451,8 +414,8 @@ export class OlmDevice { /** * Signs a message with the ed25519 key for this account. * - * @param {string} message message to be signed - * @return {Promise} base64-encoded signature + * @param message - message to be signed + * @returns base64-encoded signature */ public async sign(message: string): Promise { let result: string; @@ -469,7 +432,7 @@ export class OlmDevice { /** * Get the current (unused, unpublished) one-time keys for this account. * - * @return {object} one time keys; an object with the single property + * @returns one time keys; an object with the single property * curve25519, which is itself an object mapping key id to Curve25519 * key. */ @@ -490,7 +453,7 @@ export class OlmDevice { /** * Get the maximum number of one-time keys we can store. * - * @return {number} number of keys + * @returns number of keys */ public maxNumberOfOneTimeKeys(): number { return this.maxOneTimeKeys ?? -1; @@ -514,8 +477,8 @@ export class OlmDevice { /** * Generate some new one-time keys * - * @param {number} numKeys number of keys to generate - * @return {Promise} Resolved once the account is saved back having generated the keys + * @param numKeys - number of keys to generate + * @returns Resolved once the account is saved back having generated the keys */ public generateOneTimeKeys(numKeys: number): Promise { return this.cryptoStore.doTxn( @@ -532,7 +495,7 @@ export class OlmDevice { /** * Generate a new fallback keys * - * @return {Promise} Resolved once the account is saved back having generated the key + * @returns Resolved once the account is saved back having generated the key */ public async generateFallbackKey(): Promise { await this.cryptoStore.doTxn( @@ -576,9 +539,9 @@ export class OlmDevice { * * The new session will be stored in the cryptoStore. * - * @param {string} theirIdentityKey remote user's Curve25519 identity key - * @param {string} theirOneTimeKey remote user's one-time Curve25519 key - * @return {string} sessionId for the outbound session. + * @param theirIdentityKey - remote user's Curve25519 identity key + * @param theirOneTimeKey - remote user's one-time Curve25519 key + * @returns sessionId for the outbound session. */ public async createOutboundSession(theirIdentityKey: string, theirOneTimeKey: string): Promise { let newSessionId: string; @@ -615,15 +578,14 @@ export class OlmDevice { /** * Generate a new inbound session, given an incoming message * - * @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key - * @param {number} messageType messageType field from the received message (must be 0) - * @param {string} ciphertext base64-encoded body from the received message + * @param theirDeviceIdentityKey - remote user's Curve25519 identity key + * @param messageType - messageType field from the received message (must be 0) + * @param ciphertext - base64-encoded body from the received message * - * @return {{payload: string, session_id: string}} decrypted payload, and + * @returns decrypted payload, and * session id of new session * - * @raises {Error} if the received message was not valid (for instance, it - * didn't use a valid one-time key). + * @throws Error if the received message was not valid (for instance, it didn't use a valid one-time key). */ public async createInboundSession( theirDeviceIdentityKey: string, @@ -676,9 +638,9 @@ export class OlmDevice { /** * Get a list of known session IDs for the given device * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @return {Promise} a list of known session ids for the device + * @returns a list of known session ids for the device */ public async getSessionIdsForDevice(theirDeviceIdentityKey: string): Promise { const log = logger.withPrefix("[getSessionIdsForDevice]"); @@ -711,13 +673,13 @@ export class OlmDevice { /** * Get the right olm session id for encrypting messages to the given identity key * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @param {boolean} nowait Don't wait for an in-progress session to complete. + * @param nowait - Don't wait for an in-progress session to complete. * This should only be set to true of the calling function is the function * that marked the session as being in-progress. - * @param {PrefixedLogger} [log] A possibly customised log - * @return {Promise} session id, or null if no established session + * @param log - A possibly customised log + * @returns session id, or null if no established session */ public async getSessionIdForDevice( theirDeviceIdentityKey: string, @@ -759,12 +721,11 @@ export class OlmDevice { * the keys 'hasReceivedMessage' (true if the session has received an incoming * message and is therefore past the pre-key stage), and 'sessionId'. * - * @param {string} deviceIdentityKey Curve25519 identity key for the device - * @param {boolean} nowait Don't wait for an in-progress session to complete. + * @param deviceIdentityKey - Curve25519 identity key for the device + * @param nowait - Don't wait for an in-progress session to complete. * This should only be set to true of the calling function is the function * that marked the session as being in-progress. - * @param {Logger} [log] A possibly customised log - * @return {Array.<{sessionId: string, hasReceivedMessage: boolean}>} + * @param log - A possibly customised log */ public async getSessionInfoForDevice( deviceIdentityKey: string, @@ -813,12 +774,12 @@ export class OlmDevice { /** * Encrypt an outgoing message using an existing session * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @param {string} sessionId the id of the active session - * @param {string} payloadString payload to be encrypted and sent + * @param sessionId - the id of the active session + * @param payloadString - payload to be encrypted and sent * - * @return {Promise} ciphertext + * @returns ciphertext */ public async encryptMessage( theirDeviceIdentityKey: string, @@ -849,13 +810,13 @@ export class OlmDevice { /** * Decrypt an incoming message using an existing session * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @param {string} sessionId the id of the active session - * @param {number} messageType messageType field from the received message - * @param {string} ciphertext base64-encoded body from the received message + * @param sessionId - the id of the active session + * @param messageType - messageType field from the received message + * @param ciphertext - base64-encoded body from the received message * - * @return {Promise} decrypted payload. + * @returns decrypted payload. */ public async decryptMessage( theirDeviceIdentityKey: string, @@ -886,13 +847,13 @@ export class OlmDevice { /** * Determine if an incoming messages is a prekey message matching an existing session * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @param {string} sessionId the id of the active session - * @param {number} messageType messageType field from the received message - * @param {string} ciphertext base64-encoded body from the received message + * @param sessionId - the id of the active session + * @param messageType - messageType field from the received message + * @param ciphertext - base64-encoded body from the received message * - * @return {Promise} true if the received message is a prekey message which matches + * @returns true if the received message is a prekey message which matches * the given session. */ public async matchesSession( @@ -937,8 +898,7 @@ export class OlmDevice { /** * store an OutboundGroupSession in outboundGroupSessionStore * - * @param {Olm.OutboundGroupSession} session - * @private + * @internal */ private saveOutboundGroupSession(session: OutboundGroupSession): void { this.outboundGroupSessionStore[session.session_id()] = session.pickle(this.pickleKey); @@ -948,10 +908,8 @@ export class OlmDevice { * extract an OutboundGroupSession from outboundGroupSessionStore and call the * given function * - * @param {string} sessionId - * @param {function} func - * @return {object} result of func - * @private + * @returns result of func + * @internal */ private getOutboundGroupSession(sessionId: string, func: (session: OutboundGroupSession) => T): T { const pickled = this.outboundGroupSessionStore[sessionId]; @@ -971,7 +929,7 @@ export class OlmDevice { /** * Generate a new outbound group session * - * @return {string} sessionId for the outbound session. + * @returns sessionId for the outbound session. */ public createOutboundGroupSession(): string { const session = new global.Olm.OutboundGroupSession(); @@ -987,10 +945,10 @@ export class OlmDevice { /** * Encrypt an outgoing message with an outbound group session * - * @param {string} sessionId the id of the outboundgroupsession - * @param {string} payloadString payload to be encrypted and sent + * @param sessionId - the id of the outboundgroupsession + * @param payloadString - payload to be encrypted and sent * - * @return {string} ciphertext + * @returns ciphertext */ public encryptGroupMessage(sessionId: string, payloadString: string): string { logger.log(`encrypting msg with megolm session ${sessionId}`); @@ -1007,9 +965,9 @@ export class OlmDevice { /** * Get the session keys for an outbound group session * - * @param {string} sessionId the id of the outbound group session + * @param sessionId - the id of the outbound group session * - * @return {{chain_index: number, key: string}} current chain index, and + * @returns current chain index, and * base64-encoded secret key. */ public getOutboundGroupSessionKey(sessionId: string): IOutboundGroupSessionKey { @@ -1028,9 +986,9 @@ export class OlmDevice { * Unpickle a session from a sessionData object and invoke the given function. * The session is valid only until func returns. * - * @param {Object} sessionData Object describing the session. - * @param {function(Olm.InboundGroupSession)} func Invoked with the unpickled session - * @return {*} result of func + * @param sessionData - Object describing the session. + * @param func - Invoked with the unpickled session + * @returns result of func */ private unpickleInboundGroupSession( sessionData: InboundGroupSessionData, @@ -1048,15 +1006,12 @@ export class OlmDevice { /** * extract an InboundGroupSession from the crypto store and call the given function * - * @param {string} roomId The room ID to extract the session for, or null to fetch + * @param roomId - The room ID to extract the session for, or null to fetch * sessions for any room. - * @param {string} senderKey - * @param {string} sessionId - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @param {function(Olm.InboundGroupSession, InboundGroupSessionData)} func - * function to call. + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @param func - function to call. * - * @private + * @internal */ private getInboundGroupSession( roomId: string, @@ -1095,16 +1050,16 @@ export class OlmDevice { /** * Add an inbound group session to the session store * - * @param {string} roomId room in which this session will be used - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {Array} forwardingCurve25519KeyChain Devices involved in forwarding + * @param roomId - room in which this session will be used + * @param senderKey - base64-encoded curve25519 key of the sender + * @param forwardingCurve25519KeyChain - Devices involved in forwarding * this session to us. - * @param {string} sessionId session identifier - * @param {string} sessionKey base64-encoded secret key - * @param {Object} keysClaimed Other keys the sender claims. - * @param {boolean} exportFormat true if the megolm keys are in export format + * @param sessionId - session identifier + * @param sessionKey - base64-encoded secret key + * @param keysClaimed - Other keys the sender claims. + * @param exportFormat - true if the megolm keys are in export format * (ie, they lack an ed25519 signature) - * @param {Object} [extraSessionData={}] any other data to be include with the session + * @param extraSessionData - any other data to be include with the session */ public async addInboundGroupSession( roomId: string, @@ -1216,11 +1171,11 @@ export class OlmDevice { /** * Record in the data store why an inbound group session was withheld. * - * @param {string} roomId room that the session belongs to - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier - * @param {string} code reason code - * @param {string} reason human-readable version of `code` + * @param roomId - room that the session belongs to + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier + * @param code - reason code + * @param reason - human-readable version of `code` */ public async addInboundGroupSessionWithheld( roomId: string, @@ -1248,18 +1203,14 @@ export class OlmDevice { /** * Decrypt a received message with an inbound group session * - * @param {string} roomId room in which the message was received - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier - * @param {string} body base64-encoded body of the encrypted message - * @param {string} eventId ID of the event being decrypted - * @param {Number} timestamp timestamp of the event being decrypted - * - * @return {null} the sessionId is unknown + * @param roomId - room in which the message was received + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier + * @param body - base64-encoded body of the encrypted message + * @param eventId - ID of the event being decrypted + * @param timestamp - timestamp of the event being decrypted * - * @return {Promise<{result: string, senderKey: string, - * forwardingCurve25519KeyChain: Array, - * keysClaimed: Object}>} + * @returns null if the sessionId is unknown */ public async decryptGroupMessage( roomId: string, @@ -1375,11 +1326,11 @@ export class OlmDevice { /** * Determine if we have the keys for a given megolm session * - * @param {string} roomId room in which the message was received - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier + * @param roomId - room in which the message was received + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier * - * @returns {Promise} true if we have the keys to this session + * @returns true if we have the keys to this session */ public async hasInboundSessionKeys(roomId: string, senderKey: string, sessionId: string): Promise { let result: boolean; @@ -1418,16 +1369,13 @@ export class OlmDevice { /** * Extract the keys to a given megolm session, for sharing * - * @param {string} roomId room in which the message was received - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier - * @param {number} chainIndex The chain index at which to export the session. + * @param roomId - room in which the message was received + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier + * @param chainIndex - The chain index at which to export the session. * If omitted, export at the first index we know about. * - * @returns {Promise<{chain_index: number, key: string, - * forwarding_curve25519_key_chain: Array, - * sender_claimed_ed25519_key: string - * }>} + * @returns * details of the session key. The key is a base64-encoded megolm key in * export format. * @@ -1492,10 +1440,10 @@ export class OlmDevice { /** * Export an inbound group session * - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier - * @param {ISessionInfo} sessionData The session object from the store - * @return {module:crypto/OlmDevice.MegolmSessionData} exported session data + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier + * @param sessionData - The session object from the store + * @returns exported session data */ public exportInboundGroupSession( senderKey: string, @@ -1539,11 +1487,11 @@ export class OlmDevice { /** * Verify an ed25519 signature. * - * @param {string} key ed25519 key - * @param {string} message message which was signed - * @param {string} signature base64-encoded signature to be checked + * @param key - ed25519 key + * @param message - message which was signed + * @param signature - base64-encoded signature to be checked * - * @raises {Error} if there is a problem with the verification. If the key was + * @throws Error if there is a problem with the verification. If the key was * too small then the message will be "OLM.INVALID_BASE64". If the signature * was invalid then the message will be "OLM.BAD_MESSAGE_MAC". */ @@ -1568,11 +1516,11 @@ export const WITHHELD_MESSAGES: Record = { /** * Calculate the message to use for the exception when a session key is withheld. * - * @param {object} withheld An object that describes why the key was withheld. + * @param withheld - An object that describes why the key was withheld. * - * @return {string} the message + * @returns the message * - * @private + * @internal */ function calculateWithheldMessage(withheld: IWithheld): string { if (withheld.code && withheld.code in WITHHELD_MESSAGES) { diff --git a/src/crypto/OutgoingRoomKeyRequestManager.ts b/src/crypto/OutgoingRoomKeyRequestManager.ts index cb93851b9d1..833592a4cc1 100644 --- a/src/crypto/OutgoingRoomKeyRequestManager.ts +++ b/src/crypto/OutgoingRoomKeyRequestManager.ts @@ -27,17 +27,17 @@ import { EventType, ToDeviceMessageId } from "../@types/event"; * * See https://docs.google.com/document/d/1m4gQkcnJkxNuBmb5NoFCIadIY-DyqqNAS3lloE73BlQ * for draft documentation on what we're supposed to be implementing here. - * - * @module */ // delay between deciding we want some keys, and sending out the request, to // allow for (a) it turning up anyway, (b) grouping requests together const SEND_KEY_REQUESTS_DELAY_MS = 500; -/** possible states for a room key request +/** + * possible states for a room key request * * The state machine looks like: + * ``` * * | (cancellation sent) * | .-------------------------------------------------. @@ -60,8 +60,7 @@ const SEND_KEY_REQUESTS_DELAY_MS = 500; * | (cancellation sent) | * V | * (deleted) <---------------------------+ - * - * @enum {number} + * ``` */ export enum RoomKeyRequestState { /** request not yet sent */ @@ -134,12 +133,10 @@ export class OutgoingRoomKeyRequestManager { * Otherwise, a request is added to the pending list, and a job is started * in the background to send it. * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * @param {Array<{userId: string, deviceId: string}>} recipients - * @param {boolean} resend whether to resend the key request if there is + * @param resend - whether to resend the key request if there is * already one * - * @returns {Promise} resolves when the request has been added to the + * @returns resolves when the request has been added to the * pending list (or we have established that a similar request already * exists) */ @@ -239,9 +236,8 @@ export class OutgoingRoomKeyRequestManager { /** * Cancel room key requests, if any match the given requestBody * - * @param {module:crypto~RoomKeyRequestBody} requestBody * - * @returns {Promise} resolves when the request has been updated in our + * @returns resolves when the request has been updated in our * pending list. */ public cancelRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { @@ -324,11 +320,10 @@ export class OutgoingRoomKeyRequestManager { /** * Look for room key requests by target device and state * - * @param {string} userId Target user ID - * @param {string} deviceId Target device ID + * @param userId - Target user ID + * @param deviceId - Target device ID * - * @return {Promise} resolves to a list of all the - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to a list of all the {@link OutgoingRoomKeyRequest} */ public getOutgoingSentRoomKeyRequest(userId: string, deviceId: string): Promise { return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]); @@ -339,7 +334,7 @@ export class OutgoingRoomKeyRequestManager { * This is intended for situations where something substantial has changed, and we * don't really expect the other end to even care about the cancellation. * For example, after initialization or self-verification. - * @return {Promise} An array of `queueRoomKeyRequest` outputs. + * @returns An array of `queueRoomKeyRequest` outputs. */ public async cancelAndResendAllOutgoingRequests(): Promise { const outgoings = await this.cryptoStore.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent); diff --git a/src/crypto/RoomList.ts b/src/crypto/RoomList.ts index 672ef473c05..2f5233bd2e4 100644 --- a/src/crypto/RoomList.ts +++ b/src/crypto/RoomList.ts @@ -15,8 +15,6 @@ limitations under the License. */ /** - * @module crypto/RoomList - * * Manages the list of encrypted rooms */ @@ -31,9 +29,6 @@ export interface IRoomEncryption { } /* eslint-enable camelcase */ -/** - * @alias module:crypto/RoomList - */ export class RoomList { // Object of roomId -> room e2e info object (body of the m.room.encryption event) private roomEncryption: Record = {}; diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts index 54702034f46..921207c4a16 100644 --- a/src/crypto/SecretStorage.ts +++ b/src/crypto/SecretStorage.ts @@ -66,7 +66,6 @@ interface ISecretInfo { /** * Implements Secure Secret Storage and Sharing (MSC1946) - * @module crypto/SecretStorage */ export class SecretStorage { private requests = new Map(); @@ -119,15 +118,15 @@ export class SecretStorage { /** * Add a key for encrypting secrets. * - * @param {string} algorithm the algorithm used by the key. - * @param {object} opts the options for the algorithm. The properties used + * @param algorithm - the algorithm used by the key. + * @param opts - the options for the algorithm. The properties used * depend on the algorithm given. - * @param {string} [keyId] the ID of the key. If not given, a random + * @param keyId - the ID of the key. If not given, a random * ID will be generated. * - * @return {object} An object with: - * keyId: {string} the ID of the key - * keyInfo: {object} details about the key (iv, mac, passphrase) + * @returns An object with: + * keyId: the ID of the key + * keyInfo: details about the key (iv, mac, passphrase) */ public async addKey( algorithm: string, @@ -176,9 +175,9 @@ export class SecretStorage { /** * Get the key information for a given ID. * - * @param {string} [keyId = default key's ID] The ID of the key to check + * @param keyId - The ID of the key to check * for. Defaults to the default key ID if not provided. - * @returns {Array?} If the key was found, the return value is an array of + * @returns If the key was found, the return value is an array of * the form [keyId, keyInfo]. Otherwise, null is returned. * XXX: why is this an array when addKey returns an object? */ @@ -199,9 +198,9 @@ export class SecretStorage { /** * Check whether we have a key with a given ID. * - * @param {string} [keyId = default key's ID] The ID of the key to check + * @param keyId - The ID of the key to check * for. Defaults to the default key ID if not provided. - * @return {boolean} Whether we have the key. + * @returns Whether we have the key. */ public async hasKey(keyId?: string): Promise { return Boolean(await this.getKey(keyId)); @@ -210,10 +209,10 @@ export class SecretStorage { /** * Check whether a key matches what we expect based on the key info * - * @param {Uint8Array} key the key to check - * @param {object} info the key info + * @param key - the key to check + * @param info - the key info * - * @return {boolean} whether or not the key matches + * @returns whether or not the key matches */ public async checkKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise { if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { @@ -232,9 +231,9 @@ export class SecretStorage { /** * Store an encrypted secret on the server * - * @param {string} name The name of the secret - * @param {string} secret The secret contents. - * @param {Array} keys The IDs of the keys to use to encrypt the secret + * @param name - The name of the secret + * @param secret - The secret contents. + * @param keys - The IDs of the keys to use to encrypt the secret * or null/undefined to use the default key. */ public async store(name: string, secret: string, keys?: string[] | null): Promise { @@ -280,9 +279,9 @@ export class SecretStorage { /** * Get a secret from storage. * - * @param {string} name the name of the secret + * @param name - the name of the secret * - * @return {string} the contents of the secret + * @returns the contents of the secret */ public async get(name: string): Promise { const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); @@ -326,9 +325,9 @@ export class SecretStorage { /** * Check if a secret is stored on the server. * - * @param {string} name the name of the secret + * @param name - the name of the secret * - * @return {object?} map of key name to key info the secret is encrypted + * @returns map of key name to key info the secret is encrypted * with, or null if it is not present or not encrypted with a trusted * key */ @@ -361,8 +360,8 @@ export class SecretStorage { /** * Request a secret from another device * - * @param {string} name the name of the secret to request - * @param {string[]} devices the devices to request the secret from + * @param name - the name of the secret to request + * @param devices - the devices to request the secret from */ public request(this: SecretStorage, name: string, devices: string[]): ISecretRequest { const requestId = this.baseApis.makeTxnId(); diff --git a/src/crypto/aes.ts b/src/crypto/aes.ts index 33971e6346b..da546483439 100644 --- a/src/crypto/aes.ts +++ b/src/crypto/aes.ts @@ -22,18 +22,21 @@ const zeroSalt = new Uint8Array(8); export interface IEncryptedPayload { [key: string]: any; // extensible + /** the initialization vector in base64 */ iv: string; + /** the ciphertext in base64 */ ciphertext: string; + /** the HMAC in base64 */ mac: string; } /** * encrypt a string * - * @param {string} data the plaintext to encrypt - * @param {Uint8Array} key the encryption key to use - * @param {string} name the name of the secret - * @param {string} ivStr the initialization vector to use + * @param data - the plaintext to encrypt + * @param key - the encryption key to use + * @param name - the name of the secret + * @param ivStr - the initialization vector to use */ export async function encryptAES( data: string, @@ -83,12 +86,9 @@ export async function encryptAES( /** * decrypt a string * - * @param {object} data the encrypted data - * @param {string} data.ciphertext the ciphertext in base64 - * @param {string} data.iv the initialization vector in base64 - * @param {string} data.mac the HMAC in base64 - * @param {Uint8Array} key the encryption key to use - * @param {string} name the name of the secret + * @param data - the encrypted data + * @param key - the encryption key to use + * @param name - the name of the secret */ export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: string): Promise { const [aesKey, hmacKey] = await deriveKeys(key, name); @@ -168,10 +168,10 @@ const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0 /** Calculate the MAC for checking the key. * - * @param {Uint8Array} key the key to use - * @param {string} [iv] The initialization vector as a base64-encoded string. + * @param key - the key to use + * @param iv - The initialization vector as a base64-encoded string. * If omitted, a random initialization vector will be created. - * @return {Promise} An object that contains, `mac` and `iv` properties. + * @returns An object that contains, `mac` and `iv` properties. */ export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise { return encryptAES(ZERO_STR, key, "", iv); diff --git a/src/crypto/algorithms/base.ts b/src/crypto/algorithms/base.ts index 32b9ab73b7e..3229ad39bd2 100644 --- a/src/crypto/algorithms/base.ts +++ b/src/crypto/algorithms/base.ts @@ -16,8 +16,6 @@ limitations under the License. /** * Internal module. Defines the base classes of the encryption implementations - * - * @module */ import { MatrixClient } from "../../client"; @@ -35,46 +33,36 @@ import { DeviceInfo } from "../deviceinfo"; import { IRoomEncryption } from "../RoomList"; /** - * map of registered encryption algorithm classes. A map from string to {@link - * module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class - * - * @type {Object.} + * Map of registered encryption algorithm classes. A map from string to {@link EncryptionAlgorithm} class */ export const ENCRYPTION_CLASSES = new Map EncryptionAlgorithm>(); export type DecryptionClassParams

= Omit; /** - * map of registered encryption algorithm classes. Map from string to {@link - * module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class - * - * @type {Object.} + * map of registered encryption algorithm classes. Map from string to {@link DecryptionAlgorithm} class */ export const DECRYPTION_CLASSES = new Map DecryptionAlgorithm>(); export interface IParams { + /** The UserID for the local user */ userId: string; + /** The identifier for this device. */ deviceId: string; + /** crypto core */ crypto: Crypto; + /** olm.js wrapper */ olmDevice: OlmDevice; + /** base matrix api interface */ baseApis: MatrixClient; + /** The ID of the room we will be sending to */ roomId?: string; + /** The body of the m.room.encryption event */ config: IRoomEncryption & object; } /** * base type for encryption implementations - * - * @alias module:crypto/algorithms/base.EncryptionAlgorithm - * - * @param {object} params parameters - * @param {string} params.userId The UserID for the local user - * @param {string} params.deviceId The identifier for this device. - * @param {module:crypto} params.crypto crypto core - * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper - * @param {MatrixClient} baseApis base matrix api interface - * @param {string} params.roomId The ID of the room we will be sending to - * @param {object} params.config The body of the m.room.encryption event */ export abstract class EncryptionAlgorithm { protected readonly userId: string; @@ -84,6 +72,9 @@ export abstract class EncryptionAlgorithm { protected readonly baseApis: MatrixClient; protected readonly roomId?: string; + /** + * @param params - parameters + */ public constructor(params: IParams) { this.userId = params.userId; this.deviceId = params.deviceId; @@ -97,33 +88,28 @@ export abstract class EncryptionAlgorithm { * Perform any background tasks that can be done before a message is ready to * send, in order to speed up sending of the message. * - * @param {module:models/room} room the room the event is in + * @param room - the room the event is in */ public prepareToEncrypt(room: Room): void {} /** * Encrypt a message event * - * @method module:crypto/algorithms/base.EncryptionAlgorithm.encryptMessage * @public - * @abstract * - * @param {module:models/room} room - * @param {string} eventType - * @param {object} content event content + * @param content - event content * - * @return {Promise} Promise which resolves to the new event body + * @returns Promise which resolves to the new event body */ public abstract encryptMessage(room: Room, eventType: string, content: IContent): Promise; /** * Called when the membership of a member of the room changes. * - * @param {module:models/event.MatrixEvent} event event causing the change - * @param {module:models/room-member} member user whose membership changed - * @param {string=} oldMembership previous membership + * @param event - event causing the change + * @param member - user whose membership changed + * @param oldMembership - previous membership * @public - * @abstract */ public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {} @@ -139,15 +125,6 @@ export abstract class EncryptionAlgorithm { /** * base type for decryption implementations - * - * @alias module:crypto/algorithms/base.DecryptionAlgorithm - * @param {object} params parameters - * @param {string} params.userId The UserID for the local user - * @param {module:crypto} params.crypto crypto core - * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper - * @param {MatrixClient} baseApis base matrix api interface - * @param {string=} params.roomId The ID of the room we will be receiving - * from. Null for to-device events. */ export abstract class DecryptionAlgorithm { protected readonly userId: string; @@ -167,12 +144,9 @@ export abstract class DecryptionAlgorithm { /** * Decrypt an event * - * @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent - * @abstract - * - * @param {MatrixEvent} event undecrypted event + * @param event - undecrypted event * - * @return {Promise} promise which + * @returns promise which * resolves once we have finished decrypting. Rejects with an * `algorithms.DecryptionError` if there is a problem decrypting the event. */ @@ -181,9 +155,7 @@ export abstract class DecryptionAlgorithm { /** * Handle a key event * - * @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent - * - * @param {module:models/event.MatrixEvent} params event key event + * @param params - event key event */ public async onRoomKeyEvent(params: MatrixEvent): Promise { // ignore by default @@ -192,8 +164,7 @@ export abstract class DecryptionAlgorithm { /** * Import a room key * - * @param {module:crypto/OlmDevice.MegolmSessionData} session - * @param {object} opts object + * @param opts - object */ public async importRoomKey(session: IMegolmSessionData, opts: object): Promise { // ignore by default @@ -202,8 +173,7 @@ export abstract class DecryptionAlgorithm { /** * Determine if we have the keys necessary to respond to a room key request * - * @param {module:crypto~IncomingRoomKeyRequest} keyRequest - * @return {Promise} true if we have the keys and could (theoretically) share + * @returns true if we have the keys and could (theoretically) share * them; else false. */ public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise { @@ -213,7 +183,6 @@ export abstract class DecryptionAlgorithm { /** * Send the response to a room key request * - * @param {module:crypto~IncomingRoomKeyRequest} keyRequest */ public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void { throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm"); @@ -223,7 +192,7 @@ export abstract class DecryptionAlgorithm { * Retry decrypting all the events from a sender that haven't been * decrypted yet. * - * @param {string} senderKey the sender's key + * @param senderKey - the sender's key */ public async retryDecryptionFromSender(senderKey: string): Promise { // ignore by default @@ -237,13 +206,10 @@ export abstract class DecryptionAlgorithm { /** * Exception thrown when decryption fails * - * @alias module:crypto/algorithms/base.DecryptionError - * @param {string} msg user-visible message describing the problem + * @param msg - user-visible message describing the problem * - * @param {Object=} details key/value pairs reported in the logs but not shown + * @param details - key/value pairs reported in the logs but not shown * to the user. - * - * @extends Error */ export class DecryptionError extends Error { public readonly detailedString: string; @@ -268,16 +234,14 @@ function detailedStringForDecryptionError(err: DecryptionError, details?: Record return result; } -/** - * Exception thrown specifically when we want to warn the user to consider - * the security of their conversation before continuing - * - * @param {string} msg message describing the problem - * @param {Object} devices userId -> {deviceId -> object} - * set of unknown devices per user we're warning about - * @extends Error - */ export class UnknownDeviceError extends Error { + /** + * Exception thrown specifically when we want to warn the user to consider + * the security of their conversation before continuing + * + * @param msg - message describing the problem + * @param devices - set of unknown devices per user we're warning about + */ public constructor( msg: string, public readonly devices: Record>, @@ -292,15 +256,11 @@ export class UnknownDeviceError extends Error { /** * Registers an encryption/decryption class for a particular algorithm * - * @param {string} algorithm algorithm tag to register for + * @param algorithm - algorithm tag to register for * - * @param {class} encryptor {@link - * module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} - * implementation + * @param encryptor - {@link EncryptionAlgorithm} implementation * - * @param {class} decryptor {@link - * module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} - * implementation + * @param decryptor - {@link DecryptionAlgorithm} implementation */ export function registerAlgorithm

( algorithm: string, diff --git a/src/crypto/algorithms/index.ts b/src/crypto/algorithms/index.ts index 3dd1158a0c5..b3c5b0ede84 100644 --- a/src/crypto/algorithms/index.ts +++ b/src/crypto/algorithms/index.ts @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module crypto/algorithms - */ - import "./olm"; import "./megolm"; diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index b099ef40ae6..d995623d4ed 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -16,8 +16,6 @@ limitations under the License. /** * Defines m.olm encryption/decryption - * - * @module crypto/algorithms/megolm */ import { v4 as uuidv4 } from "uuid"; @@ -123,37 +121,27 @@ interface SharedWithData { } /** - * @private - * @constructor - * - * @param {string} sessionId - * @param {boolean} sharedHistory whether the session can be freely shared with - * other group members, according to the room history visibility settings - * - * @property {string} sessionId - * @property {Number} useCount number of times this session has been used - * @property {Number} creationTime when the session was created (ms since the epoch) - * - * @property {object} sharedWithDevices - * devices with which we have shared the session key - * userId -> {deviceId -> SharedWithData} + * @internal */ class OutboundSessionInfo { + /** number of times this session has been used */ public useCount = 0; + /** when the session was created (ms since the epoch) */ public creationTime: number; + /** devices with which we have shared the session key `userId -> {deviceId -> SharedWithData}` */ public sharedWithDevices: Record> = {}; public blockedDevicesNotified: Record> = {}; + /** + * @param sharedHistory - whether the session can be freely shared with + * other group members, according to the room history visibility settings + */ public constructor(public readonly sessionId: string, public readonly sharedHistory = false) { this.creationTime = new Date().getTime(); } /** * Check if it's time to rotate the session - * - * @param {Number} rotationPeriodMsgs - * @param {Number} rotationPeriodMs - * @return {Boolean} */ public needsRotation(rotationPeriodMsgs: number, rotationPeriodMs: number): boolean { const sessionLifetime = new Date().getTime() - this.creationTime; @@ -189,10 +177,10 @@ class OutboundSessionInfo { * Determine if this session has been shared with devices which it shouldn't * have been. * - * @param {Object} devicesInRoom userId -> {deviceId -> object} + * @param devicesInRoom - `userId -> {deviceId -> object}` * devices we should shared the session with. * - * @return {Boolean} true if we have shared the session with devices which aren't + * @returns true if we have shared the session with devices which aren't * in devicesInRoom. */ public sharedWithTooManyDevices(devicesInRoom: Record>): boolean { @@ -228,11 +216,7 @@ class OutboundSessionInfo { /** * Megolm encryption implementation * - * @constructor - * @extends {module:crypto/algorithms/EncryptionAlgorithm} - * - * @param {object} params parameters, as per - * {@link module:crypto/algorithms/EncryptionAlgorithm} + * @param params - parameters, as per {@link EncryptionAlgorithm} */ export class MegolmEncryption extends EncryptionAlgorithm { // the most recent attempt to set up a session. This is used to serialise @@ -265,15 +249,14 @@ export class MegolmEncryption extends EncryptionAlgorithm { } /** - * @private + * @internal * - * @param {module:models/room} room - * @param {Object} devicesInRoom The devices in this room, indexed by user ID - * @param {Object} blocked The devices that are blocked, indexed by user ID - * @param {boolean} [singleOlmCreationPhase] Only perform one round of olm + * @param devicesInRoom - The devices in this room, indexed by user ID + * @param blocked - The devices that are blocked, indexed by user ID + * @param singleOlmCreationPhase - Only perform one round of olm * session creation * - * @return {Promise} Promise which resolves to the + * @returns Promise which resolves to the * OutboundSessionInfo when setup is complete. */ private async ensureOutboundSession( @@ -488,11 +471,10 @@ export class MegolmEncryption extends EncryptionAlgorithm { } /** - * @private + * @internal * - * @param {boolean} sharedHistory * - * @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * @returns session */ private async prepareNewSession(sharedHistory: boolean): Promise { const sessionId = this.olmDevice.createOutboundGroupSession(); @@ -514,15 +496,15 @@ export class MegolmEncryption extends EncryptionAlgorithm { * Determines what devices in devicesByUser don't have an olm session as given * in devicemap. * - * @private + * @internal * - * @param {object} devicemap the devices that have olm sessions, as returned by + * @param devicemap - the devices that have olm sessions, as returned by * olmlib.ensureOlmSessionsForDevices. - * @param {object} devicesByUser a map of user IDs to array of deviceInfo - * @param {array} [noOlmDevices] an array to fill with devices that don't have + * @param devicesByUser - a map of user IDs to array of deviceInfo + * @param noOlmDevices - an array to fill with devices that don't have * olm sessions * - * @return {array} an array of devices that don't have olm sessions. If + * @returns an array of devices that don't have olm sessions. If * noOlmDevices is specified, then noOlmDevices will be returned. */ private getDevicesWithoutSessions( @@ -558,11 +540,11 @@ export class MegolmEncryption extends EncryptionAlgorithm { * Splits the user device map into multiple chunks to reduce the number of * devices we encrypt to per API call. * - * @private + * @internal * - * @param {object} devicesByUser map from userid to list of devices + * @param devicesByUser - map from userid to list of devices * - * @return {array>} the blocked devices, split into chunks + * @returns the blocked devices, split into chunks */ private splitDevices( devicesByUser: Record>, @@ -599,18 +581,16 @@ export class MegolmEncryption extends EncryptionAlgorithm { } /** - * @private + * @internal * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {number} chainIndex current chain index + * @param chainIndex - current chain index * - * @param {object} userDeviceMap - * mapping from userId to deviceInfo + * @param userDeviceMap - mapping from userId to deviceInfo * - * @param {object} payload fields to include in the encrypted payload + * @param payload - fields to include in the encrypted payload * - * @return {Promise} Promise which resolves once the key sharing + * @returns Promise which resolves once the key sharing * for the given userDeviceMap is generated and has been sent. */ private encryptAndSendKeysToDevices( @@ -639,15 +619,14 @@ export class MegolmEncryption extends EncryptionAlgorithm { } /** - * @private + * @internal * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {array} userDeviceMap list of blocked devices to notify + * @param userDeviceMap - list of blocked devices to notify * - * @param {object} payload fields to include in the notification payload + * @param payload - fields to include in the notification payload * - * @return {Promise} Promise which resolves once the notifications + * @returns Promise which resolves once the notifications * for the given userDeviceMap is generated and has been sent. */ private async sendBlockedNotificationsToDevices( @@ -695,10 +674,10 @@ export class MegolmEncryption extends EncryptionAlgorithm { * Re-shares a megolm session key with devices if the key has already been * sent to them. * - * @param {string} senderKey The key of the originating device for the session - * @param {string} sessionId ID of the outbound session to share - * @param {string} userId ID of the user who owns the target device - * @param {module:crypto/deviceinfo} device The target device + * @param senderKey - The key of the originating device for the session + * @param sessionId - ID of the outbound session to share + * @param userId - ID of the user who owns the target device + * @param device - The target device */ public async reshareKeyWithDevice( senderKey: string, @@ -792,26 +771,23 @@ export class MegolmEncryption extends EncryptionAlgorithm { } /** - * @private + * @internal * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {object} key the session key as returned by + * @param key - the session key as returned by * OlmDevice.getOutboundGroupSessionKey * - * @param {object} payload the base to-device message payload for sharing keys + * @param payload - the base to-device message payload for sharing keys * - * @param {object} devicesByUser - * map from userid to list of devices + * @param devicesByUser - map from userid to list of devices * - * @param {array} errorDevices - * array that will be populated with the devices that we can't get an + * @param errorDevices - array that will be populated with the devices that we can't get an * olm session for * - * @param {Number} [otkTimeout] The timeout in milliseconds when requesting + * @param otkTimeout - The timeout in milliseconds when requesting * one-time keys for establishing new olm sessions. * - * @param {Array} [failedServers] An array to fill with remote servers that + * @param failedServers - An array to fill with remote servers that * failed to respond to one-time-key requests. */ private async shareKeyWithDevices( @@ -866,11 +842,9 @@ export class MegolmEncryption extends EncryptionAlgorithm { /** * Notify devices that we weren't able to create olm sessions. * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {object} key * - * @param {Array} failedDevices the devices that we were unable to + * @param failedDevices - the devices that we were unable to * create olm sessions for, as returned by shareKeyWithDevices */ private async notifyFailedOlmDevices( @@ -927,10 +901,8 @@ export class MegolmEncryption extends EncryptionAlgorithm { /** * Notify blocked devices that they have been blocked. * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {object} devicesByUser - * map from userid to device ID to blocked data + * @param devicesByUser - map from userid to device ID to blocked data */ private async notifyBlockedDevices( session: OutboundSessionInfo, @@ -963,7 +935,7 @@ export class MegolmEncryption extends EncryptionAlgorithm { * Perform any background tasks that can be done before a message is ready to * send, in order to speed up sending of the message. * - * @param {module:models/room} room the room the event is in + * @param room - the room the event is in */ public prepareToEncrypt(room: Room): void { if (this.encryptionPreparation != null) { @@ -1008,13 +980,9 @@ export class MegolmEncryption extends EncryptionAlgorithm { } /** - * @inheritdoc - * - * @param {module:models/room} room - * @param {string} eventType - * @param {object} content plaintext event content + * @param content - plaintext event content * - * @return {Promise} Promise which resolves to the new event body + * @returns Promise which resolves to the new event body */ public async encryptMessage(room: Room, eventType: string, content: IContent): Promise { logger.log(`Starting to encrypt event for ${this.roomId}`); @@ -1103,7 +1071,7 @@ export class MegolmEncryption extends EncryptionAlgorithm { * unknown to the user. If so, warn the user, and mark them as known to * give the user a chance to go verify them before re-sending this message. * - * @param {Object} devicesInRoom userId -> {deviceId -> object} + * @param devicesInRoom - `userId -> {deviceId -> object}` * devices we should shared the session with. */ private checkForUnknownDevices(devicesInRoom: DeviceInfoMap): void { @@ -1133,7 +1101,7 @@ export class MegolmEncryption extends EncryptionAlgorithm { * Remove unknown devices from a set of devices. The devicesInRoom parameter * will be modified. * - * @param {Object} devicesInRoom userId -> {deviceId -> object} + * @param devicesInRoom - `userId -> {deviceId -> object}` * devices we should shared the session with. */ private removeUnknownDevices(devicesInRoom: DeviceInfoMap): void { @@ -1153,11 +1121,10 @@ export class MegolmEncryption extends EncryptionAlgorithm { /** * Get the list of unblocked devices for all users in the room * - * @param {module:models/room} room - * @param forceDistributeToUnverified if set to true will include the unverified devices + * @param forceDistributeToUnverified - if set to true will include the unverified devices * even if setting is set to block them (useful for verification) * - * @return {Promise} Promise which resolves to an array whose + * @returns Promise which resolves to an array whose * first element is a map from userId to deviceId to deviceInfo indicating * the devices that messages should be encrypted to, and whose second * element is a map from userId to deviceId to data indicating the devices @@ -1225,11 +1192,7 @@ export class MegolmEncryption extends EncryptionAlgorithm { /** * Megolm decryption implementation * - * @constructor - * @extends {module:crypto/algorithms/DecryptionAlgorithm} - * - * @param {object} params parameters, as per - * {@link module:crypto/algorithms/DecryptionAlgorithm} + * @param params - parameters, as per {@link DecryptionAlgorithm} */ export class MegolmDecryption extends DecryptionAlgorithm { // events which we couldn't decrypt due to unknown sessions / @@ -1248,12 +1211,8 @@ export class MegolmDecryption extends DecryptionAlgorithm { } /** - * @inheritdoc - * - * @param {MatrixEvent} event - * * returns a promise which resolves to a - * {@link module:crypto~EventDecryptionResult} once we have finished + * {@link EventDecryptionResult} once we have finished * decrypting, or rejects with an `algorithms.DecryptionError` if there is a * problem decrypting the event. */ @@ -1394,9 +1353,8 @@ export class MegolmDecryption extends DecryptionAlgorithm { /** * Add an event to the list of those awaiting their session keys. * - * @private + * @internal * - * @param {module:models/event.MatrixEvent} event */ private addEventToPendingList(event: MatrixEvent): void { const content = event.getWireContent(); @@ -1415,9 +1373,8 @@ export class MegolmDecryption extends DecryptionAlgorithm { /** * Remove an event from the list of those awaiting their session keys. * - * @private + * @internal * - * @param {module:models/event.MatrixEvent} event */ private removeEventFromPendingList(event: MatrixEvent): void { const content = event.getWireContent(); @@ -1438,11 +1395,6 @@ export class MegolmDecryption extends DecryptionAlgorithm { } } - /** - * @inheritdoc - * - * @param {module:models/event.MatrixEvent} event key event - */ public async onRoomKeyEvent(event: MatrixEvent): Promise { const content = event.getContent>(); let senderKey = event.getSenderKey()!; @@ -1619,9 +1571,7 @@ export class MegolmDecryption extends DecryptionAlgorithm { } /** - * @inheritdoc - * - * @param {module:models/event.MatrixEvent} event key event + * @param event - key event */ public async onRoomKeyWithheldEvent(event: MatrixEvent): Promise { const content = event.getContent(); @@ -1706,9 +1656,6 @@ export class MegolmDecryption extends DecryptionAlgorithm { } } - /** - * @inheritdoc - */ public hasKeysForKeyRequest(keyRequest: IncomingRoomKeyRequest): Promise { const body = keyRequest.requestBody; @@ -1720,9 +1667,6 @@ export class MegolmDecryption extends DecryptionAlgorithm { ); } - /** - * @inheritdoc - */ public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void { const userId = keyRequest.userId; const deviceId = keyRequest.deviceId; @@ -1808,19 +1752,15 @@ export class MegolmDecryption extends DecryptionAlgorithm { } /** - * @inheritdoc - * - * @param {module:crypto/OlmDevice.MegolmSessionData} session - * @param {object} [opts={}] options for the import - * @param {boolean} [opts.untrusted] whether the key should be considered as untrusted - * @param {string} [opts.source] where the key came from + * @param untrusted - whether the key should be considered as untrusted + * @param source - where the key came from */ public importRoomKey( session: IMegolmSessionData, - opts: { untrusted?: boolean, source?: string } = {}, + { untrusted, source }: { untrusted?: boolean, source?: string } = {}, ): Promise { const extraSessionData: OlmGroupSessionExtraData = {}; - if (opts.untrusted || session.untrusted) { + if (untrusted || session.untrusted) { extraSessionData.untrusted = true; } if (session["org.matrix.msc3061.shared_history"]) { @@ -1836,7 +1776,7 @@ export class MegolmDecryption extends DecryptionAlgorithm { true, extraSessionData, ).then(() => { - if (opts.source !== "backup") { + if (source !== "backup") { // don't wait for it to complete this.crypto.backupManager.backupGroupSession( session.sender_key, session.session_id, @@ -1855,13 +1795,11 @@ export class MegolmDecryption extends DecryptionAlgorithm { * Have another go at decrypting events after we receive a key. Resolves once * decryption has been re-attempted on all events. * - * @private - * @param {String} senderKey - * @param {String} sessionId - * @param {Boolean} forceRedecryptIfUntrusted whether messages that were already + * @internal + * @param forceRedecryptIfUntrusted - whether messages that were already * successfully decrypted using untrusted keys should be re-decrypted * - * @return {Boolean} whether all messages were successfully + * @returns whether all messages were successfully * decrypted with trusted keys */ private async retryDecryption( diff --git a/src/crypto/algorithms/olm.ts b/src/crypto/algorithms/olm.ts index 114368319b7..1feee6ba6cf 100644 --- a/src/crypto/algorithms/olm.ts +++ b/src/crypto/algorithms/olm.ts @@ -16,8 +16,6 @@ limitations under the License. /** * Defines m.olm encryption/decryption - * - * @module crypto/algorithms/olm */ import { logger } from '../../logger'; @@ -44,21 +42,17 @@ export interface IMessage { /** * Olm encryption implementation * - * @constructor - * @extends {module:crypto/algorithms/EncryptionAlgorithm} - * - * @param {object} params parameters, as per - * {@link module:crypto/algorithms/EncryptionAlgorithm} + * @param params - parameters, as per {@link EncryptionAlgorithm} */ class OlmEncryption extends EncryptionAlgorithm { private sessionPrepared = false; private prepPromise: Promise | null = null; /** - * @private + * @internal - * @param {string[]} roomMembers list of currently-joined users in the room - * @return {Promise} Promise which resolves when setup is complete + * @param roomMembers - list of currently-joined users in the room + * @returns Promise which resolves when setup is complete */ private ensureSession(roomMembers: string[]): Promise { if (this.prepPromise) { @@ -83,13 +77,9 @@ class OlmEncryption extends EncryptionAlgorithm { } /** - * @inheritdoc - * - * @param {module:models/room} room - * @param {string} eventType - * @param {object} content plaintext event content + * @param content - plaintext event content * - * @return {Promise} Promise which resolves to the new event body + * @returns Promise which resolves to the new event body */ public async encryptMessage(room: Room, eventType: string, content: IContent): Promise { // pick the list of recipients based on the membership list. @@ -150,19 +140,12 @@ class OlmEncryption extends EncryptionAlgorithm { /** * Olm decryption implementation * - * @constructor - * @extends {module:crypto/algorithms/DecryptionAlgorithm} - * @param {object} params parameters, as per - * {@link module:crypto/algorithms/DecryptionAlgorithm} + * @param params - parameters, as per {@link DecryptionAlgorithm} */ class OlmDecryption extends DecryptionAlgorithm { /** - * @inheritdoc - * - * @param {MatrixEvent} event - * * returns a promise which resolves to a - * {@link module:crypto~EventDecryptionResult} once we have finished + * {@link EventDecryptionResult} once we have finished * decrypting. Rejects with an `algorithms.DecryptionError` if there is a * problem decrypting the event. */ @@ -275,10 +258,10 @@ class OlmDecryption extends DecryptionAlgorithm { /** * Attempt to decrypt an Olm message * - * @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender - * @param {object} message message object, with 'type' and 'body' fields + * @param theirDeviceIdentityKey - Curve25519 identity key of the sender + * @param message - message object, with 'type' and 'body' fields * - * @return {string} payload, if decrypted successfully. + * @returns payload, if decrypted successfully. */ private decryptMessage(theirDeviceIdentityKey: string, message: IMessage): Promise { // This is a wrapper that serialises decryptions of prekey messages, because diff --git a/src/crypto/api.ts b/src/crypto/api.ts index f6487ca913d..468cc993383 100644 --- a/src/crypto/api.ts +++ b/src/crypto/api.ts @@ -66,8 +66,7 @@ export interface IRecoveryKey { export interface ICreateSecretStorageOpts { /** * Function called to await a secret storage key creation flow. - * Returns: - * {Promise} Object with public key metadata, encoded private + * @returns Promise resolving to an object with public key metadata, encoded private * recovery key which should be disposed of after displaying to the user, * and raw private key to avoid round tripping if needed. */ @@ -131,6 +130,7 @@ export interface IImportOpts { } export interface IImportRoomKeysOpts { + /** called with an object that has a "stage" param */ progressCallback?: (stage: IImportOpts) => void; untrusted?: boolean; source?: string; // TODO: Enum diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 51912893605..fa90c6b7865 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -15,8 +15,6 @@ limitations under the License. */ /** - * @module crypto/backup - * * Classes for dealing with key backup. */ @@ -134,7 +132,7 @@ export class BackupManager { * * Throws an error if a problem is detected. * - * @param {IKeyBackupInfo} info the key backup info + * @param info - the key backup info */ public static checkBackupVersion(info: IKeyBackupInfo): void { const Algorithm = algorithmsByName[info.algorithm]; @@ -276,7 +274,7 @@ export class BackupManager { * Forces a re-check of the key backup and enables/disables it * as appropriate. * - * @return {Object} Object with backup info (as returned by + * @returns Object with backup info (as returned by * getKeyBackupVersion) in backupInfo and * trust information (as returned by isKeyBackupTrusted) * in trustInfo. @@ -309,15 +307,7 @@ export class BackupManager { /** * Check if the given backup info is trusted. * - * @param {IKeyBackupInfo} backupInfo key backup info dict from /room_keys/version - * @return {object} { - * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device - * sigs: [ - * valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation - * deviceId: [string], - * device: [DeviceInfo || null], - * ] - * } + * @param backupInfo - key backup info dict from /room_keys/version */ public async isKeyBackupTrusted(backupInfo?: IKeyBackupInfo): Promise { const ret = { @@ -432,7 +422,7 @@ export class BackupManager { * Schedules sending all keys waiting to be sent to the backup, if not already * scheduled. Retries if necessary. * - * @param maxDelay Maximum delay to wait in ms. 0 means no delay. + * @param maxDelay - Maximum delay to wait in ms. 0 means no delay. */ public async scheduleKeyBackupSend(maxDelay = 10000): Promise { if (this.sendingBackups) return; @@ -490,8 +480,8 @@ export class BackupManager { * Take some e2e keys waiting to be backed up and send them * to the backup. * - * @param {number} limit Maximum number of keys to back up - * @returns {number} Number of sessions backed up + * @param limit - Maximum number of keys to back up + * @returns Number of sessions backed up */ public async backupPendingKeys(limit: number): Promise { const sessions = await this.baseApis.crypto!.cryptoStore.getSessionsNeedingBackup(limit); @@ -573,7 +563,7 @@ export class BackupManager { /** * Marks all group sessions as needing to be backed up without scheduling * them to upload in the background. - * @returns {Promise} Resolves to the number of sessions now requiring a backup + * @returns Promise which resolves to the number of sessions now requiring a backup * (which will be equal to the number of sessions in the store). */ public async flagAllGroupSessionsForBackup(): Promise { @@ -599,7 +589,7 @@ export class BackupManager { /** * Counts the number of end to end session keys that are waiting to be backed up - * @returns {Promise} Resolves to the number of sessions requiring backup + * @returns Promise which resolves to the number of sessions requiring backup */ public countSessionsNeedingBackup(): Promise { return this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup(); diff --git a/src/crypto/deviceinfo.ts b/src/crypto/deviceinfo.ts index a8fd9f0086b..1fbbd280044 100644 --- a/src/crypto/deviceinfo.ts +++ b/src/crypto/deviceinfo.ts @@ -16,10 +16,6 @@ limitations under the License. import { ISignatures } from "../@types/signed"; -/** - * @module crypto/deviceinfo - */ - export interface IDevice { keys: Record; algorithms: string[]; @@ -37,36 +33,15 @@ enum DeviceVerification { /** * Information about a user's device - * - * @constructor - * @alias module:crypto/deviceinfo - * - * @property {string} deviceId the ID of this device - * - * @property {string[]} algorithms list of algorithms supported by this device - * - * @property {Object.} keys a map from - * <key type>:<id> -> <base64-encoded key>> - * - * @property {module:crypto/deviceinfo.DeviceVerification} verified - * whether the device has been verified/blocked by the user - * - * @property {boolean} known - * whether the user knows of this device's existence (useful when warning - * the user that a user has added new devices) - * - * @property {Object} unsigned additional data from the homeserver - * - * @param {string} deviceId id of the device */ export class DeviceInfo { /** * rehydrate a DeviceInfo from the session store * - * @param {object} obj raw object from session store - * @param {string} deviceId id of the device + * @param obj - raw object from session store + * @param deviceId - id of the device * - * @return {module:crypto~DeviceInfo} new DeviceInfo + * @returns new DeviceInfo */ public static fromStorage(obj: Partial, deviceId: string): DeviceInfo { const res = new DeviceInfo(deviceId); @@ -79,28 +54,36 @@ export class DeviceInfo { return res; } - /** - * @enum - */ public static DeviceVerification = { VERIFIED: DeviceVerification.Verified, UNVERIFIED: DeviceVerification.Unverified, BLOCKED: DeviceVerification.Blocked, }; + /** list of algorithms supported by this device */ public algorithms: string[] = []; + /** a map from `: -> ` */ public keys: Record = {}; + /** whether the device has been verified/blocked by the user */ public verified = DeviceVerification.Unverified; + /** + * whether the user knows of this device's existence + * (useful when warning the user that a user has added new devices) + */ public known = false; + /** additional data from the homeserver */ public unsigned: Record = {}; public signatures: ISignatures = {}; + /** + * @param deviceId - id of the device + */ public constructor(public readonly deviceId: string) {} /** * Prepare a DeviceInfo for JSON serialisation in the session store * - * @return {object} deviceinfo with non-serialised members removed + * @returns deviceinfo with non-serialised members removed */ public toStorage(): IDevice { return { @@ -116,7 +99,7 @@ export class DeviceInfo { /** * Get the fingerprint for this device (ie, the Ed25519 key) * - * @return {string} base64-encoded fingerprint of this device + * @returns base64-encoded fingerprint of this device */ public getFingerprint(): string { return this.keys["ed25519:" + this.deviceId]; @@ -125,7 +108,7 @@ export class DeviceInfo { /** * Get the identity key for this device (ie, the Curve25519 key) * - * @return {string} base64-encoded identity key of this device + * @returns base64-encoded identity key of this device */ public getIdentityKey(): string { return this.keys["curve25519:" + this.deviceId]; @@ -134,7 +117,7 @@ export class DeviceInfo { /** * Get the configured display name for this device, if any * - * @return {string?} displayname + * @returns displayname */ public getDisplayName(): string | null { return this.unsigned.device_display_name || null; @@ -143,7 +126,7 @@ export class DeviceInfo { /** * Returns true if this device is blocked * - * @return {Boolean} true if blocked + * @returns true if blocked */ public isBlocked(): boolean { return this.verified == DeviceVerification.Blocked; @@ -152,7 +135,7 @@ export class DeviceInfo { /** * Returns true if this device is verified * - * @return {Boolean} true if verified + * @returns true if verified */ public isVerified(): boolean { return this.verified == DeviceVerification.Verified; @@ -161,7 +144,7 @@ export class DeviceInfo { /** * Returns true if this device is unverified * - * @return {Boolean} true if unverified + * @returns true if unverified */ public isUnverified(): boolean { return this.verified == DeviceVerification.Unverified; @@ -170,7 +153,7 @@ export class DeviceInfo { /** * Returns true if the user knows about this device's existence * - * @return {Boolean} true if known + * @returns true if known */ public isKnown(): boolean { return this.known === true; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 9163ace4f71..65b43a635a0 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -17,10 +17,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module crypto - */ - import anotherjson from "another-json"; import { v4 as uuidv4 } from "uuid"; @@ -128,7 +124,12 @@ interface IInitOpts { } export interface IBootstrapCrossSigningOpts { + /** Optional. Reset even if keys already exist. */ setupNewCrossSigning?: boolean; + /** + * A function that makes the request requiring auth. Receives the auth data as an object. + * Can be called multiple times, first with an empty authDict, to obtain the flows. + */ authUploadDeviceSigningKeys?(makeRequest: (authData: any) => Promise<{}>): Promise; } @@ -161,18 +162,32 @@ interface IRoomKey { algorithm: string; } +/** + * The parameters of a room key request. The details of the request may + * vary with the crypto algorithm, but the management and storage layers for + * outgoing requests expect it to have 'room_id' and 'session_id' properties. + */ export interface IRoomKeyRequestBody extends IRoomKey { session_id: string; sender_key: string; } -export interface IMegolmSessionData { - [key: string]: any; // extensible +interface Extensible { + [key: string]: any; +} + +export interface IMegolmSessionData extends Extensible { + // Sender's Curve25519 device key sender_key: string; + // Devices which forwarded this session to us (normally empty). forwarding_curve25519_key_chain: string[]; + // Other keys the sender claims. sender_claimed_keys: Record; + // Room this session is used in room_id: string; + // Unique id for the session session_id: string; + // Base64'ed key data session_key: string; algorithm?: string; untrusted?: boolean; @@ -188,13 +203,6 @@ export interface ICheckOwnCrossSigningTrustOpts { allowPrivateKeyRequests?: boolean; } -/** - * @typedef {Object} module:crypto~OlmSessionResult - * @property {module:crypto/deviceinfo} device device info - * @property {string?} sessionId base64 olm session id; null if no session - * could be established - */ - interface IUserOlmSession { deviceIdKey: string; sessions: { @@ -213,10 +221,26 @@ interface ISignableObject { unsigned?: object; } +/** + * The result of a (successful) call to decryptEvent. + */ export interface IEventDecryptionResult { + /** + * The plaintext payload for the event (typically containing type and content fields). + */ clearEvent: IClearEvent; + /** + * List of curve25519 keys involved in telling us about the senderCurve25519Key and claimedEd25519Key. + * See {@link MatrixEvent#getForwardingCurve25519KeyChain}. + */ forwardingCurve25519KeyChain?: string[]; + /** + * Key owned by the sender of this event. See {@link MatrixEvent#getSenderKey}. + */ senderCurve25519Key?: string; + /** + * ed25519 key claimed by the sender of this event. See {@link MatrixEvent#getClaimedEd25519Key}. + */ claimedEd25519Key?: string; untrusted?: boolean; } @@ -266,10 +290,50 @@ export enum CryptoEvent { } export type CryptoEventHandlerMap = { + /** + * Fires when a device is marked as verified/unverified/blocked/unblocked by + * {@link MatrixClient#setDeviceVerified|MatrixClient.setDeviceVerified} or + * {@link MatrixClient#setDeviceBlocked|MatrixClient.setDeviceBlocked}. + * + * @param userId - the owner of the verified device + * @param deviceId - the id of the verified device + * @param deviceInfo - updated device information + */ [CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, device: DeviceInfo) => void; + /** + * Fires when the trust status of a user changes + * If userId is the userId of the logged-in user, this indicated a change + * in the trust status of the cross-signing data on the account. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * @experimental + * + * @param userId - the userId of the user in question + * @param trustLevel - The new trust level of the user + */ [CryptoEvent.UserTrustStatusChanged]: (userId: string, trustLevel: UserTrustLevel) => void; + /** + * Fires when we receive a room key request + * + * @param req - request details + */ [CryptoEvent.RoomKeyRequest]: (request: IncomingRoomKeyRequest) => void; + /** + * Fires when we receive a room key request cancellation + */ [CryptoEvent.RoomKeyRequestCancellation]: (request: IncomingRoomKeyRequestCancellation) => void; + /** + * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled() + * @param enabled - true if key backup has been enabled, otherwise false + * @example + * ``` + * matrixClient.on("crypto.keyBackupStatus", function(enabled){ + * if (enabled) { + * [...] + * } + * }); + * ``` + */ [CryptoEvent.KeyBackupStatus]: (enabled: boolean) => void; [CryptoEvent.KeyBackupFailed]: (errcode: string) => void; [CryptoEvent.KeyBackupSessionsRemaining]: (remaining: number) => void; @@ -277,18 +341,50 @@ export type CryptoEventHandlerMap = { failures: IUploadKeySignaturesResponse["failures"], source: "checkOwnCrossSigningTrust" | "afterCrossSigningLocalKeyChange" | "setDeviceVerification", upload: (opts: { shouldEmit: boolean }) => Promise - ) => void; + ) => void;/** + * Fires when a key verification is requested. + */ [CryptoEvent.VerificationRequest]: (request: VerificationRequest) => void; + /** + * Fires when the app may wish to warn the user about something related + * the end-to-end crypto. + * + * @param type - One of the strings listed above + */ [CryptoEvent.Warning]: (type: string) => void; + /** + * Fires when the user's cross-signing keys have changed or cross-signing + * has been enabled/disabled. The client can use getStoredCrossSigningForUser + * with the user ID of the logged in user to check if cross-signing is + * enabled on the account. If enabled, it can test whether the current key + * is trusted using with checkUserTrust with the user ID of the logged + * in user. The checkOwnCrossSigningTrust function may be used to reconcile + * the trust in the account key. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * @experimental + */ [CryptoEvent.KeysChanged]: (data: {}) => void; + /** + * Fires whenever the stored devices for a user will be updated + * @param users - A list of user IDs that will be updated + * @param initialFetch - If true, the store is empty (apart + * from our own device) and is being seeded. + */ [CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void; + /** + * Fires whenever the stored devices for a user have changed + * @param users - A list of user IDs that were updated + * @param initialFetch - If true, the store was empty (apart + * from our own device) and has been seeded. + */ [CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void; [CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void; }; export class Crypto extends TypedEventEmitter { /** - * @return {string} The version of Olm. + * @returns The version of Olm. */ public static getOlmVersion(): [number, number, number] { return OlmDevice.getOlmVersion(); @@ -362,25 +458,21 @@ export class Crypto extends TypedEventEmitter { @@ -556,7 +647,7 @@ export class Crypto extends TypedEventEmitter} Object with public key metadata, encoded private + * @returns Object with public key metadata, encoded private * recovery key which should be disposed of after displaying to the user, * and raw private key to avoid round tripping if needed. */ @@ -639,7 +730,7 @@ export class Crypto extends TypedEventEmitter { const publicKeysOnDevice = this.crossSigningInfo.getId(); @@ -664,7 +755,7 @@ export class Crypto extends TypedEventEmitter { const secretStorageKeyInAccount = await this.secretStorage.hasKey(); @@ -694,12 +785,12 @@ export class Crypto extends TypedEventEmitter} Object with public key metadata, encoded private + * Returns a Promise which resolves to an object with public key metadata, encoded private * recovery key which should be disposed of after displaying to the user, * and raw private key to avoid round tripping if needed. - * @param {object} [opts.keyBackupInfo] The current key backup object. If passed, + * @param keyBackupInfo - The current key backup object. If passed, * the passphrase and recovery key from this backup will be used. - * @param {boolean} [opts.setupNewKeyBackup] If true, a new key backup version will be + * @param setupNewKeyBackup - If true, a new key backup version will be * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo * is supplied. - * @param {boolean} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist. - * @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's + * @param setupNewSecretStorage - Optional. Reset even if keys already exist. + * @param getKeyBackupPassphrase - Optional. Function called to get the user's * current key backup passphrase. Should return a promise that resolves with a Buffer * containing the key, or rejects if the key cannot be obtained. * Returns: - * {Promise} A promise which resolves to key creation data for + * A promise which resolves to key creation data for * SecretStorage#addKey: an object with `passphrase` etc fields. */ // TODO this does not resolve with what it says it does @@ -1150,9 +1240,9 @@ export class Crypto extends TypedEventEmitter { let key = await new Promise((resolve) => { // TODO types @@ -1200,8 +1290,8 @@ export class Crypto extends TypedEventEmitter): Promise { if (!(key instanceof Uint8Array)) { @@ -1223,9 +1313,9 @@ export class Crypto extends TypedEventEmitter | null): Promise { if (keys) { @@ -1732,7 +1822,7 @@ export class Crypto extends TypedEventEmitter { const shouldUpgradeCb = ( @@ -1774,7 +1864,7 @@ export class Crypto extends TypedEventEmitter { const deviceKeys = { @@ -1866,7 +1956,7 @@ export class Crypto extends TypedEventEmitterdeviceId->{@link - * module:crypto/deviceinfo|DeviceInfo}. + * @returns A promise which resolves to a map `userId->deviceId->{@link DeviceInfo}`. */ public downloadKeys(userIds: string[], forceDownload?: boolean): Promise { return this.deviceList.downloadKeys(userIds, !!forceDownload); @@ -2069,9 +2158,9 @@ export class Crypto extends TypedEventEmitter | null { @@ -2081,10 +2170,8 @@ export class Crypto extends TypedEventEmitter} true if the data was saved, false if + * @returns true if the data was saved, false if * it was not (eg. because no changes were pending). The promise * will only resolve once the data is saved, so may take some time * to resolve. @@ -2110,24 +2197,24 @@ export class Crypto extends TypedEventEmitter} keys The list of keys that was present + * @param keys - The list of keys that was present * during the device verification. This will be double checked with the list * of keys the given device has currently. * - * @return {Promise} updated DeviceInfo + * @returns updated DeviceInfo */ public async setDeviceVerification( userId: string, @@ -2392,13 +2479,11 @@ export class Crypto extends TypedEventEmitter * This method is provided for debugging purposes. * - * @param {string} userId id of user to inspect - * - * @return {Promise>} + * @param userId - id of user to inspect */ public async getOlmSessionsForUser(userId: string): Promise> { const devices = this.getStoredDevicesForUser(userId) || []; @@ -2418,9 +2503,7 @@ export class Crypto extends TypedEventEmitternot initiate a device list query for the room. That is normally * done once we finish processing the sync, in onSyncCompleted. * - * @param room The room to enable encryption in. - * @param config The encryption config for the room. + * @param room - The room to enable encryption in. + * @param config - The encryption config for the room. */ private async setRoomEncryptionImpl( room: Room, @@ -2682,8 +2765,8 @@ export class Crypto extends TypedEventEmitter { const room = this.clientStore.getRoom(roomId); @@ -2701,7 +2784,7 @@ export class Crypto extends TypedEventEmitter { const roomId = room.roomId; @@ -2732,12 +2815,12 @@ export class Crypto extends TypedEventEmitter { const exportedSessions: IMegolmSessionData[] = []; @@ -2793,10 +2876,8 @@ export class Crypto extends TypedEventEmitter { let successes = 0; @@ -2830,7 +2911,7 @@ export class Crypto extends TypedEventEmitter} Resolves to the number of sessions requiring backup + * @returns Promise which resolves to the number of sessions requiring backup */ public countSessionsNeedingBackup(): Promise { return this.backupManager.countSessionsNeedingBackup(); @@ -2840,7 +2921,7 @@ export class Crypto extends TypedEventEmitter { @@ -2917,9 +2998,8 @@ export class Crypto extends TypedEventEmitter} resolves once we have + * @returns resolves once we have * finished decrypting. Rejects with an `algorithms.DecryptionError` if there * is a problem decrypting the event. */ @@ -2952,8 +3032,8 @@ export class Crypto extends TypedEventEmitter} recipients - * @param {boolean} resend whether to resend the key request if there is + * @param resend - whether to resend the key request if there is * already one * - * @return {Promise} a promise that resolves when the key request is queued + * @returns a promise that resolves when the key request is queued */ public requestRoomKey( requestBody: IRoomKeyRequestBody, @@ -3007,8 +3085,7 @@ export class Crypto extends TypedEventEmitter { await this.outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests(); @@ -3028,8 +3105,8 @@ export class Crypto extends TypedEventEmitter { const content = event.getContent(); @@ -3039,7 +3116,7 @@ export class Crypto extends TypedEventEmitter { if (!syncData.oldSyncToken) { @@ -3063,7 +3140,7 @@ export class Crypto extends TypedEventEmitter { this.deviceList.setSyncToken(syncData.nextSyncToken ?? null); @@ -3096,7 +3173,7 @@ export class Crypto extends TypedEventEmitter["device_lists"]): Promise { @@ -3124,7 +3201,7 @@ export class Crypto extends TypedEventEmitter { const e2eUserIds: string[] = []; @@ -3141,7 +3218,7 @@ export class Crypto extends TypedEventEmitter { @@ -3163,11 +3240,11 @@ export class Crypto extends TypedEventEmitter} Promise which + * @param userDeviceInfoArr - the devices to send to + * @param payload - fields to include in the encrypted payload + * @returns Promise which * resolves once the message has been encrypted and sent to the given - * userDeviceMap, and returns the { contentMap, deviceInfoByDeviceId } + * userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }` * of the successfully sent messages. */ public async encryptAndSendToDevices( @@ -3281,8 +3358,8 @@ export class Crypto extends TypedEventEmitter { const content = event.getWireContent(); @@ -3565,10 +3642,10 @@ export class Crypto extends TypedEventEmitter { if (this.processingRoomKeyRequests) { @@ -3669,7 +3746,6 @@ export class Crypto extends TypedEventEmitter { const userId = req.userId; @@ -3759,7 +3835,6 @@ export class Crypto extends TypedEventEmitter | undefined; @@ -3834,9 +3906,9 @@ export class Crypto extends TypedEventEmitter(obj: T): Promise { const sigs = obj.signatures || {}; @@ -3874,8 +3946,8 @@ export class Crypto extends TypedEventEmitter void; public constructor(event: MatrixEvent) { @@ -3928,14 +3992,13 @@ export class IncomingRoomKeyRequest { /** * Represents a received m.room_key_request cancellation - * - * @property {string} userId user requesting the cancellation - * @property {string} deviceId device requesting the cancellation - * @property {string} requestId unique id for the request to be cancelled */ class IncomingRoomKeyRequestCancellation { + /** user requesting the cancellation */ public readonly userId: string; + /** device requesting the cancellation */ public readonly deviceId: string; + /** unique id for the request to be cancelled */ public readonly requestId: string; public constructor(event: MatrixEvent) { @@ -3946,46 +4009,3 @@ class IncomingRoomKeyRequestCancellation { this.requestId = content.request_id; } } - -/** - * The result of a (successful) call to decryptEvent. - * - * @typedef {Object} EventDecryptionResult - * - * @property {Object} clearEvent The plaintext payload for the event - * (typically containing type and content fields). - * - * @property {?string} senderCurve25519Key Key owned by the sender of this - * event. See {@link module:models/event.MatrixEvent#getSenderKey}. - * - * @property {?string} claimedEd25519Key ed25519 key claimed by the sender of - * this event. See - * {@link module:models/event.MatrixEvent#getClaimedEd25519Key}. - * - * @property {?Array} forwardingCurve25519KeyChain list of curve25519 - * keys involved in telling us about the senderCurve25519Key and - * claimedEd25519Key. See - * {@link module:models/event.MatrixEvent#getForwardingCurve25519KeyChain}. - */ - -/** - * Fires when we receive a room key request - * - * @event module:client~MatrixClient#"crypto.roomKeyRequest" - * @param {module:crypto~IncomingRoomKeyRequest} req request details - */ - -/** - * Fires when we receive a room key request cancellation - * - * @event module:client~MatrixClient#"crypto.roomKeyRequestCancellation" - * @param {module:crypto~IncomingRoomKeyRequestCancellation} req - */ - -/** - * Fires when the app may wish to warn the user about something related - * the end-to-end crypto. - * - * @event module:client~MatrixClient#"crypto.warning" - * @param {string} type One of the strings listed above - */ diff --git a/src/crypto/keybackup.ts b/src/crypto/keybackup.ts index 919266a3ef8..67e213c4a92 100644 --- a/src/crypto/keybackup.ts +++ b/src/crypto/keybackup.ts @@ -59,6 +59,10 @@ export interface IKeyBackupInfo { /* eslint-enable camelcase */ export interface IKeyBackupPrepareOpts { + /** + * Whether to use Secure Secret Storage to store the key encrypting key backups. + * Optional, defaults to false. + */ secureSecretStorage: boolean; } diff --git a/src/crypto/olmlib.ts b/src/crypto/olmlib.ts index 862691353c9..4a228e2814a 100644 --- a/src/crypto/olmlib.ts +++ b/src/crypto/olmlib.ts @@ -15,8 +15,6 @@ limitations under the License. */ /** - * @module olmlib - * * Utilities common to olm encryption algorithms */ @@ -55,22 +53,20 @@ export const MEGOLM_ALGORITHM = Algorithm.Megolm; export const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup; export interface IOlmSessionResult { + /** device info */ device: DeviceInfo; + /** base64 olm session id; null if no session could be established */ sessionId: string | null; } /** * Encrypt an event payload for an Olm device * - * @param {Object} resultsObject The `ciphertext` property + * @param resultsObject - The `ciphertext` property * of the m.room.encrypted event to which to add our result * - * @param {string} ourUserId - * @param {string} ourDeviceId - * @param {module:crypto/OlmDevice} olmDevice olm.js wrapper - * @param {string} recipientUserId - * @param {module:crypto/deviceinfo} recipientDevice - * @param {object} payloadFields fields to include in the encrypted payload + * @param olmDevice - olm.js wrapper + * @param payloadFields - fields to include in the encrypted payload * * Returns a promise which resolves (to undefined) when the payload * has been encrypted into `resultsObject` @@ -145,17 +141,14 @@ interface IExistingOlmSession { * Get the existing olm sessions for the given devices, and the devices that * don't have olm sessions. * - * @param {module:crypto/OlmDevice} olmDevice * - * @param {MatrixClient} baseApis * - * @param {object} devicesByUser - * map from userid to list of devices to ensure sessions for + * @param devicesByUser - map from userid to list of devices to ensure sessions for * - * @return {Promise} resolves to an array. The first element of the array is a + * @returns resolves to an array. The first element of the array is a * a map of user IDs to arrays of deviceInfo, representing the devices that * don't have established olm sessions. The second element of the array is - * a map from userId to deviceId to {@link module:crypto~OlmSessionResult} + * a map from userId to deviceId to {@link OlmSessionResult} */ export async function getExistingOlmSessions( olmDevice: OlmDevice, @@ -197,27 +190,22 @@ export async function getExistingOlmSessions( /** * Try to make sure we have established olm sessions for the given devices. * - * @param {module:crypto/OlmDevice} olmDevice - * - * @param {MatrixClient} baseApis - * - * @param {object} devicesByUser - * map from userid to list of devices to ensure sessions for + * @param devicesByUser - map from userid to list of devices to ensure sessions for * - * @param {boolean} [force=false] If true, establish a new session even if one + * @param force - If true, establish a new session even if one * already exists. * - * @param {Number} [otkTimeout] The timeout in milliseconds when requesting + * @param otkTimeout - The timeout in milliseconds when requesting * one-time keys for establishing new olm sessions. * - * @param {Array} [failedServers] An array to fill with remote servers that + * @param failedServers - An array to fill with remote servers that * failed to respond to one-time-key requests. * - * @param {Logger} [log] A possibly customised log + * @param log - A possibly customised log * - * @return {Promise} resolves once the sessions are complete, to + * @returns resolves once the sessions are complete, to * an Object mapping from userId to deviceId to - * {@link module:crypto~OlmSessionResult} + * {@link OlmSessionResult} */ export async function ensureOlmSessionsForDevices( olmDevice: OlmDevice, @@ -442,15 +430,15 @@ export interface IObject { /** * Verify the signature on an object * - * @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op + * @param olmDevice - olm wrapper to use for verify op * - * @param {Object} obj object to check signature on. + * @param obj - object to check signature on. * - * @param {string} signingUserId ID of the user whose signature should be checked + * @param signingUserId - ID of the user whose signature should be checked * - * @param {string} signingDeviceId ID of the device whose signature should be checked + * @param signingDeviceId - ID of the device whose signature should be checked * - * @param {string} signingKey base64-ed ed25519 public key + * @param signingKey - base64-ed ed25519 public key * * Returns a promise which resolves (to undefined) if the the signature is good, * or rejects with an Error if it is bad. @@ -485,13 +473,13 @@ export async function verifySignature( /** * Sign a JSON object using public key cryptography - * @param {Object} obj Object to sign. The object will be modified to include + * @param obj - Object to sign. The object will be modified to include * the new signature - * @param {Olm.PkSigning|Uint8Array} key the signing object or the private key + * @param key - the signing object or the private key * seed - * @param {string} userId The user ID who owns the signing key - * @param {string} pubKey The public key (ignored if key is a seed) - * @returns {string} the signature for the object + * @param userId - The user ID who owns the signing key + * @param pubKey - The public key (ignored if key is a seed) + * @returns the signature for the object */ export function pkSign(obj: object & IObject, key: Uint8Array | PkSigning, userId: string, pubKey: string): string { let createdKey = false; @@ -521,9 +509,9 @@ export function pkSign(obj: object & IObject, key: Uint8Array | PkSigning, userI /** * Verify a signed JSON object - * @param {Object} obj Object to verify - * @param {string} pubKey The public key to use to verify - * @param {string} userId The user ID who signed the object + * @param obj - Object to verify + * @param pubKey - The public key to use to verify + * @param userId - The user ID who signed the object */ export function pkVerify(obj: IObject, pubKey: string, userId: string): void { const keyId = "ed25519:" + pubKey; @@ -563,8 +551,8 @@ export function isOlmEncrypted(event: MatrixEvent): boolean { /** * Encode a typed array of uint8 as base64. - * @param {Uint8Array} uint8Array The data to encode. - * @return {string} The base64. + * @param uint8Array - The data to encode. + * @returns The base64. */ export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string { return Buffer.from(uint8Array).toString("base64"); @@ -572,8 +560,8 @@ export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string { /** * Encode a typed array of uint8 as unpadded base64. - * @param {Uint8Array} uint8Array The data to encode. - * @return {string} The unpadded base64. + * @param uint8Array - The data to encode. + * @returns The unpadded base64. */ export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string { return encodeBase64(uint8Array).replace(/=+$/g, ''); @@ -581,8 +569,8 @@ export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): stri /** * Decode a base64 string to a typed array of uint8. - * @param {string} base64 The base64 to decode. - * @return {Uint8Array} The decoded data. + * @param base64 - The base64 to decode. + * @returns The decoded data. */ export function decodeBase64(base64: string): Uint8Array { return Buffer.from(base64, "base64"); diff --git a/src/crypto/store/base.ts b/src/crypto/store/base.ts index 72b02e0a1cb..31ece3b066f 100644 --- a/src/crypto/store/base.ts +++ b/src/crypto/store/base.ts @@ -30,8 +30,6 @@ import { IEncryptedPayload } from "../aes"; /** * Internal module. Definitions for storage for the crypto module - * - * @module */ export interface SecretStorePrivateKeys { @@ -46,8 +44,6 @@ export interface SecretStorePrivateKeys { /** * Abstraction of things that can store data required for end-to-end encryption - * - * @interface CryptoStore */ export interface CryptoStore { startup(): Promise; @@ -197,32 +193,29 @@ export interface IWithheld { /** * Represents an outgoing room key request - * - * @typedef {Object} OutgoingRoomKeyRequest - * - * @property {string} requestId unique id for this request. Used for both - * an id within the request for later pairing with a cancellation, and for - * the transaction id when sending the to_device messages to our local - * server. - * - * @property {string?} cancellationTxnId - * transaction id for the cancellation, if any - * - * @property {Array<{userId: string, deviceId: string}>} recipients - * list of recipients for the request - * - * @property {module:crypto~RoomKeyRequestBody} requestBody - * parameters for the request. - * - * @property {Number} state current state of this request (states are defined - * in {@link module:crypto/OutgoingRoomKeyRequestManager~ROOM_KEY_REQUEST_STATES}) */ export interface OutgoingRoomKeyRequest { + /** + * Unique id for this request. Used for both an id within the request for later pairing with a cancellation, + * and for the transaction id when sending the to_device messages to our local server. + */ requestId: string; requestTxnId?: string; + /** + * Transaction id for the cancellation, if any + */ cancellationTxnId?: string; + /** + * List of recipients for the request + */ recipients: IRoomKeyRequestRecipient[]; + /** + * Parameters for the request + */ requestBody: IRoomKeyRequestBody; + /** + * current state of this request (states are defined in {@link OutgoingRoomKeyRequestManager}) + */ state: RoomKeyRequestState; } diff --git a/src/crypto/store/indexeddb-crypto-store-backend.ts b/src/crypto/store/indexeddb-crypto-store-backend.ts index cdf35e787a5..3a0c7711a9a 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.ts +++ b/src/crypto/store/indexeddb-crypto-store-backend.ts @@ -39,14 +39,11 @@ const PROFILE_TRANSACTIONS = false; * Implementation of a CryptoStore which is backed by an existing * IndexedDB connection. Generally you want IndexedDBCryptoStore * which connects to the database and defers to one of these. - * - * @implements {module:crypto/store/base~CryptoStore} */ export class Backend implements CryptoStore { private nextTxnId = 0; /** - * @param {IDBDatabase} db */ public constructor(private db: IDBDatabase) { // make sure we close the db on `onversionchange` - otherwise @@ -71,10 +68,9 @@ export class Backend implements CryptoStore { * Look for an existing outgoing room key request, and if none is found, * add a new one * - * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * @returns resolves to + * {@link OutgoingRoomKeyRequest}: either the * same instance as passed in, or the existing one. */ public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise { @@ -113,11 +109,10 @@ export class Backend implements CryptoStore { /** * Look for an existing room key request * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for + * @param requestBody - existing request to look for * - * @return {Promise} resolves to the matching - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the matching + * {@link OutgoingRoomKeyRequest}, or null if * not found */ public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { @@ -134,13 +129,12 @@ export class Backend implements CryptoStore { /** * look for an existing room key request in the db * - * @private - * @param {IDBTransaction} txn database transaction - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for - * @param {Function} callback function to call with the results of the + * @internal + * @param txn - database transaction + * @param requestBody - existing request to look for + * @param callback - function to call with the results of the * search. Either passed a matching - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * {@link OutgoingRoomKeyRequest}, or null if * not found. */ // eslint-disable-next-line @typescript-eslint/naming-convention @@ -181,10 +175,10 @@ export class Backend implements CryptoStore { /** * Look for room key requests by state * - * @param {Array} wantedStates list of acceptable states + * @param wantedStates - list of acceptable states * - * @return {Promise} resolves to the a - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the a + * {@link OutgoingRoomKeyRequest}, or null if * there are no pending requests in those states. If there are multiple * requests in those states, an arbitrary one is chosen. */ @@ -233,8 +227,7 @@ export class Backend implements CryptoStore { /** * - * @param {Number} wantedState - * @return {Promise>} All elements in a given state + * @returns All elements in a given state */ public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise { return new Promise((resolve, reject) => { @@ -294,12 +287,12 @@ export class Backend implements CryptoStore { * Look for an existing room key request by id and state, and update it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in - * @param {Object} updates name/value map of updates to apply + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in + * @param updates - name/value map of updates to apply * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to + * {@link OutgoingRoomKeyRequest} * updated request, or null if no matching row was found */ public updateOutgoingRoomKeyRequest( @@ -337,10 +330,10 @@ export class Backend implements CryptoStore { * Look for an existing room key request by id and state, and delete it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in * - * @returns {Promise} resolves once the operation is completed + * @returns resolves once the operation is completed */ public deleteOutgoingRoomKeyRequest( requestId: string, diff --git a/src/crypto/store/indexeddb-crypto-store.ts b/src/crypto/store/indexeddb-crypto-store.ts index d743a95decc..ac2964d12af 100644 --- a/src/crypto/store/indexeddb-crypto-store.ts +++ b/src/crypto/store/indexeddb-crypto-store.ts @@ -39,15 +39,11 @@ import { InboundGroupSessionData } from "../OlmDevice"; /** * Internal module. indexeddb storage for e2e. - * - * @module */ /** * An implementation of CryptoStore, which is normally backed by an indexeddb, * but with fallback to MemoryCryptoStore. - * - * @implements {module:crypto/store/base~CryptoStore} */ export class IndexedDBCryptoStore implements CryptoStore { public static STORE_ACCOUNT = 'account'; @@ -70,8 +66,8 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Create a new IndexedDBCryptoStore * - * @param {IDBFactory} indexedDB global indexedDB instance - * @param {string} dbName name of db to connect to + * @param indexedDB - global indexedDB instance + * @param dbName - name of db to connect to */ public constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {} @@ -81,7 +77,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * * This must be called before the store can be used. * - * @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend, + * @returns resolves to either an IndexedDBCryptoStoreBackend.Backend, * or a MemoryCryptoStore */ public startup(): Promise { @@ -167,7 +163,7 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Delete all data from this store. * - * @returns {Promise} resolves when the store has been cleared. + * @returns resolves when the store has been cleared. */ public deleteAllData(): Promise { return new Promise((resolve, reject) => { @@ -206,10 +202,9 @@ export class IndexedDBCryptoStore implements CryptoStore { * Look for an existing outgoing room key request, and if none is found, * add a new one * - * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * @returns resolves to + * {@link OutgoingRoomKeyRequest}: either the * same instance as passed in, or the existing one. */ public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise { @@ -219,11 +214,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Look for an existing room key request * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for + * @param requestBody - existing request to look for * - * @return {Promise} resolves to the matching - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the matching + * {@link OutgoingRoomKeyRequest}, or null if * not found */ public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { @@ -233,10 +227,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Look for room key requests by state * - * @param {Array} wantedStates list of acceptable states + * @param wantedStates - list of acceptable states * - * @return {Promise} resolves to the a - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the a + * {@link OutgoingRoomKeyRequest}, or null if * there are no pending requests in those states. If there are multiple * requests in those states, an arbitrary one is chosen. */ @@ -248,8 +242,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * Look for room key requests by state – * unlike above, return a list of all entries in one state. * - * @param {Number} wantedState - * @return {Promise>} Returns an array of requests in the given state + * @returns Returns an array of requests in the given state */ public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise { return this.backend!.getAllOutgoingRoomKeyRequestsByState(wantedState); @@ -258,12 +251,12 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Look for room key requests by target device and state * - * @param {string} userId Target user ID - * @param {string} deviceId Target device ID - * @param {Array} wantedStates list of acceptable states + * @param userId - Target user ID + * @param deviceId - Target device ID + * @param wantedStates - list of acceptable states * - * @return {Promise} resolves to a list of all the - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to a list of all the + * {@link OutgoingRoomKeyRequest} */ public getOutgoingRoomKeyRequestsByTarget( userId: string, @@ -279,12 +272,12 @@ export class IndexedDBCryptoStore implements CryptoStore { * Look for an existing room key request by id and state, and update it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in - * @param {Object} updates name/value map of updates to apply + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in + * @param updates - name/value map of updates to apply * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to + * {@link OutgoingRoomKeyRequest} * updated request, or null if no matching row was found */ public updateOutgoingRoomKeyRequest( @@ -301,10 +294,10 @@ export class IndexedDBCryptoStore implements CryptoStore { * Look for an existing room key request by id and state, and delete it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in * - * @returns {Promise} resolves once the operation is completed + * @returns resolves once the operation is completed */ public deleteOutgoingRoomKeyRequest( requestId: string, @@ -319,8 +312,8 @@ export class IndexedDBCryptoStore implements CryptoStore { * Get the account pickle from the store. * This requires an active transaction. See doTxn(). * - * @param {*} txn An active transaction. See doTxn(). - * @param {function(string)} func Called with the account pickle + * @param txn - An active transaction. See doTxn(). + * @param func - Called with the account pickle */ public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void { this.backend!.getAccount(txn, func); @@ -330,8 +323,8 @@ export class IndexedDBCryptoStore implements CryptoStore { * Write the account pickle to the store. * This requires an active transaction. See doTxn(). * - * @param {*} txn An active transaction. See doTxn(). - * @param {string} accountPickle The new account pickle to store. + * @param txn - An active transaction. See doTxn(). + * @param accountPickle - The new account pickle to store. */ public storeAccount(txn: IDBTransaction, accountPickle: string): void { this.backend!.storeAccount(txn, accountPickle); @@ -341,9 +334,9 @@ export class IndexedDBCryptoStore implements CryptoStore { * Get the public part of the cross-signing keys (eg. self-signing key, * user signing key). * - * @param {*} txn An active transaction. See doTxn(). - * @param {function(string)} func Called with the account keys object: - * { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed + * @param txn - An active transaction. See doTxn(). + * @param func - Called with the account keys object: + * `{ key_type: base64 encoded seed }` where key type = user_signing_key_seed or self_signing_key_seed */ public getCrossSigningKeys( txn: IDBTransaction, @@ -353,9 +346,9 @@ export class IndexedDBCryptoStore implements CryptoStore { } /** - * @param {*} txn An active transaction. See doTxn(). - * @param {function(string)} func Called with the private key - * @param {string} type A key type + * @param txn - An active transaction. See doTxn(). + * @param func - Called with the private key + * @param type - A key type */ public getSecretStorePrivateKey( txn: IDBTransaction, @@ -368,8 +361,8 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Write the cross-signing keys back to the store * - * @param {*} txn An active transaction. See doTxn(). - * @param {string} keys keys object as getCrossSigningKeys() + * @param txn - An active transaction. See doTxn(). + * @param keys - keys object as getCrossSigningKeys() */ public storeCrossSigningKeys(txn: IDBTransaction, keys: Record): void { this.backend!.storeCrossSigningKeys(txn, keys); @@ -378,9 +371,9 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Write the cross-signing private keys back to the store * - * @param {*} txn An active transaction. See doTxn(). - * @param {string} type The type of cross-signing private key to store - * @param {string} key keys object as getCrossSigningKeys() + * @param txn - An active transaction. See doTxn(). + * @param type - The type of cross-signing private key to store + * @param key - keys object as getCrossSigningKeys() */ public storeSecretStorePrivateKey( txn: IDBTransaction, @@ -394,8 +387,8 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Returns the number of end-to-end sessions in the store - * @param {*} txn An active transaction. See doTxn(). - * @param {function(int)} func Called with the count of sessions + * @param txn - An active transaction. See doTxn(). + * @param func - Called with the count of sessions */ public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void { this.backend!.countEndToEndSessions(txn, func); @@ -404,10 +397,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Retrieve a specific end-to-end session between the logged-in user * and another device. - * @param {string} deviceKey The public key of the other device. - * @param {string} sessionId The ID of the session to retrieve - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called with A map from sessionId + * @param deviceKey - The public key of the other device. + * @param sessionId - The ID of the session to retrieve + * @param txn - An active transaction. See doTxn(). + * @param func - Called with A map from sessionId * to session information object with 'session' key being the * Base64 end-to-end session and lastReceivedMessageTs being the * timestamp in milliseconds at which the session last received @@ -425,9 +418,9 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Retrieve the end-to-end sessions between the logged-in user and another * device. - * @param {string} deviceKey The public key of the other device. - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called with A map from sessionId + * @param deviceKey - The public key of the other device. + * @param txn - An active transaction. See doTxn(). + * @param func - Called with A map from sessionId * to session information object with 'session' key being the * Base64 end-to-end session and lastReceivedMessageTs being the * timestamp in milliseconds at which the session last received @@ -443,8 +436,8 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Retrieve all end-to-end sessions - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called one for each session with + * @param txn - An active transaction. See doTxn(). + * @param func - Called one for each session with * an object with, deviceKey, lastReceivedMessageTs, sessionId * and session keys. */ @@ -454,10 +447,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Store a session between the logged-in user and another device - * @param {string} deviceKey The public key of the other device. - * @param {string} sessionId The ID for this end-to-end session. - * @param {string} sessionInfo Session information object - * @param {*} txn An active transaction. See doTxn(). + * @param deviceKey - The public key of the other device. + * @param sessionId - The ID for this end-to-end session. + * @param sessionInfo - Session information object + * @param txn - An active transaction. See doTxn(). */ public storeEndToEndSession( deviceKey: string, @@ -485,10 +478,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Retrieve the end-to-end inbound group session for a given * server key and session ID - * @param {string} senderCurve25519Key The sender's curve 25519 key - * @param {string} sessionId The ID of the session - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called with A map from sessionId + * @param senderCurve25519Key - The sender's curve 25519 key + * @param sessionId - The ID of the session + * @param txn - An active transaction. See doTxn(). + * @param func - Called with A map from sessionId * to Base64 end-to-end session. */ public getEndToEndInboundGroupSession( @@ -502,10 +495,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Fetches all inbound group sessions in the store - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called once for each group session - * in the store with an object having keys {senderKey, sessionId, - * sessionData}, then once with null to indicate the end of the list. + * @param txn - An active transaction. See doTxn(). + * @param func - Called once for each group session + * in the store with an object having keys `{senderKey, sessionId, sessionData}`, + * then once with null to indicate the end of the list. */ public getAllEndToEndInboundGroupSessions( txn: IDBTransaction, @@ -518,10 +511,10 @@ export class IndexedDBCryptoStore implements CryptoStore { * Adds an end-to-end inbound group session to the store. * If there already exists an inbound group session with the same * senderCurve25519Key and sessionID, the session will not be added. - * @param {string} senderCurve25519Key The sender's curve 25519 key - * @param {string} sessionId The ID of the session - * @param {object} sessionData The session data structure - * @param {*} txn An active transaction. See doTxn(). + * @param senderCurve25519Key - The sender's curve 25519 key + * @param sessionId - The ID of the session + * @param sessionData - The session data structure + * @param txn - An active transaction. See doTxn(). */ public addEndToEndInboundGroupSession( senderCurve25519Key: string, @@ -536,10 +529,10 @@ export class IndexedDBCryptoStore implements CryptoStore { * Writes an end-to-end inbound group session to the store. * If there already exists an inbound group session with the same * senderCurve25519Key and sessionID, it will be overwritten. - * @param {string} senderCurve25519Key The sender's curve 25519 key - * @param {string} sessionId The ID of the session - * @param {object} sessionData The session data structure - * @param {*} txn An active transaction. See doTxn(). + * @param senderCurve25519Key - The sender's curve 25519 key + * @param sessionId - The ID of the session + * @param sessionData - The session data structure + * @param txn - An active transaction. See doTxn(). */ public storeEndToEndInboundGroupSession( senderCurve25519Key: string, @@ -568,8 +561,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * These all need to be written out in full each time such that the snapshot * is always consistent, so they are stored in one object. * - * @param {Object} deviceData - * @param {*} txn An active transaction. See doTxn(). + * @param txn - An active transaction. See doTxn(). */ public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void { this.backend!.storeEndToEndDeviceData(deviceData, txn); @@ -578,8 +570,8 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Get the state of all tracked devices * - * @param {*} txn An active transaction. See doTxn(). - * @param {function(Object)} func Function called with the + * @param txn - An active transaction. See doTxn(). + * @param func - Function called with the * device data */ public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void { @@ -590,18 +582,18 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Store the end-to-end state for a room. - * @param {string} roomId The room's ID. - * @param {object} roomInfo The end-to-end info for the room. - * @param {*} txn An active transaction. See doTxn(). + * @param roomId - The room's ID. + * @param roomInfo - The end-to-end info for the room. + * @param txn - An active transaction. See doTxn(). */ public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void { this.backend!.storeEndToEndRoom(roomId, roomInfo, txn); } /** - * Get an object of roomId->roomInfo for all e2e rooms in the store - * @param {*} txn An active transaction. See doTxn(). - * @param {function(Object)} func Function called with the end to end encrypted rooms + * Get an object of `roomId->roomInfo` for all e2e rooms in the store + * @param txn - An active transaction. See doTxn(). + * @param func - Function called with the end-to-end encrypted rooms */ public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record) => void): void { this.backend!.getEndToEndRooms(txn, func); @@ -611,9 +603,9 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Get the inbound group sessions that need to be backed up. - * @param {number} limit The maximum number of sessions to retrieve. 0 + * @param limit - The maximum number of sessions to retrieve. 0 * for no limit. - * @returns {Promise} resolves to an array of inbound group sessions + * @returns resolves to an array of inbound group sessions */ public getSessionsNeedingBackup(limit: number): Promise { return this.backend!.getSessionsNeedingBackup(limit); @@ -621,8 +613,8 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Count the inbound group sessions that need to be backed up. - * @param {*} txn An active transaction. See doTxn(). (optional) - * @returns {Promise} resolves to the number of sessions + * @param txn - An active transaction. See doTxn(). (optional) + * @returns resolves to the number of sessions */ public countSessionsNeedingBackup(txn?: IDBTransaction): Promise { return this.backend!.countSessionsNeedingBackup(txn); @@ -630,9 +622,9 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Unmark sessions as needing to be backed up. - * @param {Array} sessions The sessions that need to be backed up. - * @param {*} txn An active transaction. See doTxn(). (optional) - * @returns {Promise} resolves when the sessions are unmarked + * @param sessions - The sessions that need to be backed up. + * @param txn - An active transaction. See doTxn(). (optional) + * @returns resolves when the sessions are unmarked */ public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise { return this.backend!.unmarkSessionsNeedingBackup(sessions, txn); @@ -640,9 +632,9 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Mark sessions as needing to be backed up. - * @param {Array} sessions The sessions that need to be backed up. - * @param {*} txn An active transaction. See doTxn(). (optional) - * @returns {Promise} resolves when the sessions are marked + * @param sessions - The sessions that need to be backed up. + * @param txn - An active transaction. See doTxn(). (optional) + * @returns resolves when the sessions are marked */ public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise { return this.backend!.markSessionsNeedingBackup(sessions, txn); @@ -650,10 +642,10 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Add a shared-history group session for a room. - * @param {string} roomId The room that the key belongs to - * @param {string} senderKey The sender's curve 25519 key - * @param {string} sessionId The ID of the session - * @param {*} txn An active transaction. See doTxn(). (optional) + * @param roomId - The room that the key belongs to + * @param senderKey - The sender's curve 25519 key + * @param sessionId - The ID of the session + * @param txn - An active transaction. See doTxn(). (optional) */ public addSharedHistoryInboundGroupSession( roomId: string, @@ -666,9 +658,9 @@ export class IndexedDBCryptoStore implements CryptoStore { /** * Get the shared-history group session for a room. - * @param {string} roomId The room that the key belongs to - * @param {*} txn An active transaction. See doTxn(). (optional) - * @returns {Promise} Resolves to an array of [senderKey, sessionId] + * @param roomId - The room that the key belongs to + * @param txn - An active transaction. See doTxn(). (optional) + * @returns Promise which resolves to an array of [senderKey, sessionId] */ public getSharedHistoryInboundGroupSessions( roomId: string, @@ -704,16 +696,16 @@ export class IndexedDBCryptoStore implements CryptoStore { * only be called within a callback of either this function or * one of the store functions operating on the same transaction. * - * @param {string} mode 'readwrite' if you need to call setter + * @param mode - 'readwrite' if you need to call setter * functions with this transaction. Otherwise, 'readonly'. - * @param {string[]} stores List IndexedDBCryptoStore.STORE_* + * @param stores - List IndexedDBCryptoStore.STORE_* * options representing all types of object that will be * accessed or written to with this transaction. - * @param {function(*)} func Function called with the + * @param func - Function called with the * transaction object: an opaque object that should be passed * to store functions. - * @param {Logger} [log] A possibly customised log - * @return {Promise} Promise that resolves with the result of the `func` + * @param log - A possibly customised log + * @returns Promise that resolves with the result of the `func` * when the transaction is complete. If the backend is * async (ie. the indexeddb backend) any of the callback * functions throwing an exception will cause this promise to diff --git a/src/crypto/store/localStorage-crypto-store.ts b/src/crypto/store/localStorage-crypto-store.ts index c6f9a02d6ae..04815cf1576 100644 --- a/src/crypto/store/localStorage-crypto-store.ts +++ b/src/crypto/store/localStorage-crypto-store.ts @@ -28,8 +28,6 @@ import { InboundGroupSessionData } from "../OlmDevice"; * some things backed by localStorage. It exists because indexedDB * is broken in Firefox private mode or set to, "will not remember * history". - * - * @module */ const E2E_PREFIX = "crypto."; @@ -62,9 +60,6 @@ function keyEndToEndRoomsPrefix(roomId: string): string { return KEY_ROOMS_PREFIX + roomId; } -/** - * @implements {module:crypto/store/base~CryptoStore} - */ export class LocalStorageCryptoStore extends MemoryCryptoStore { public static exists(store: Storage): boolean { const length = store.length; @@ -364,7 +359,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { /** * Delete all data from this store. * - * @returns {Promise} Promise which resolves when the store has been cleared. + * @returns Promise which resolves when the store has been cleared. */ public deleteAllData(): Promise { this.store.removeItem(KEY_END_TO_END_ACCOUNT); diff --git a/src/crypto/store/memory-crypto-store.ts b/src/crypto/store/memory-crypto-store.ts index f22379ee81e..701f85fc30a 100644 --- a/src/crypto/store/memory-crypto-store.ts +++ b/src/crypto/store/memory-crypto-store.ts @@ -35,13 +35,8 @@ import { InboundGroupSessionData } from "../OlmDevice"; /** * Internal module. in-memory storage for e2e. - * - * @module */ -/** - * @implements {module:crypto/store/base~CryptoStore} - */ export class MemoryCryptoStore implements CryptoStore { private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = []; private account: string | null = null; @@ -65,7 +60,7 @@ export class MemoryCryptoStore implements CryptoStore { * * This must be called before the store can be used. * - * @return {Promise} resolves to the store. + * @returns resolves to the store. */ public async startup(): Promise { // No startup work to do for the memory store. @@ -75,7 +70,7 @@ export class MemoryCryptoStore implements CryptoStore { /** * Delete all data from this store. * - * @returns {Promise} Promise which resolves when the store has been cleared. + * @returns Promise which resolves when the store has been cleared. */ public deleteAllData(): Promise { return Promise.resolve(); @@ -85,10 +80,9 @@ export class MemoryCryptoStore implements CryptoStore { * Look for an existing outgoing room key request, and if none is found, * add a new one * - * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * @returns resolves to + * {@link OutgoingRoomKeyRequest}: either the * same instance as passed in, or the existing one. */ public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise { @@ -122,11 +116,10 @@ export class MemoryCryptoStore implements CryptoStore { /** * Look for an existing room key request * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for + * @param requestBody - existing request to look for * - * @return {Promise} resolves to the matching - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the matching + * {@link OutgoingRoomKeyRequest}, or null if * not found */ public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { @@ -138,10 +131,9 @@ export class MemoryCryptoStore implements CryptoStore { * * @internal * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for + * @param requestBody - existing request to look for * - * @return {module:crypto/store/base~OutgoingRoomKeyRequest?} + * @returns * the matching request, or null if not found */ // eslint-disable-next-line @typescript-eslint/naming-convention @@ -157,10 +149,10 @@ export class MemoryCryptoStore implements CryptoStore { /** * Look for room key requests by state * - * @param {Array} wantedStates list of acceptable states + * @param wantedStates - list of acceptable states * - * @return {Promise} resolves to the a - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the a + * {@link OutgoingRoomKeyRequest}, or null if * there are no pending requests in those states */ public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise { @@ -176,8 +168,7 @@ export class MemoryCryptoStore implements CryptoStore { /** * - * @param {Number} wantedState - * @return {Promise>} All OutgoingRoomKeyRequests in state + * @returns All OutgoingRoomKeyRequests in state */ public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise { return Promise.resolve( @@ -210,12 +201,12 @@ export class MemoryCryptoStore implements CryptoStore { * Look for an existing room key request by id and state, and update it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in - * @param {Object} updates name/value map of updates to apply + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in + * @param updates - name/value map of updates to apply * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to + * {@link OutgoingRoomKeyRequest} * updated request, or null if no matching row was found */ public updateOutgoingRoomKeyRequest( @@ -246,10 +237,10 @@ export class MemoryCryptoStore implements CryptoStore { * Look for an existing room key request by id and state, and delete it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in * - * @returns {Promise} resolves once the operation is completed + * @returns resolves once the operation is completed */ public deleteOutgoingRoomKeyRequest( requestId: string, diff --git a/src/crypto/verification/Base.ts b/src/crypto/verification/Base.ts index 55b349e99c9..f2644b55708 100644 --- a/src/crypto/verification/Base.ts +++ b/src/crypto/verification/Base.ts @@ -17,7 +17,6 @@ limitations under the License. /** * Base class for verification methods. - * @module crypto/verification/Base */ import { MatrixEvent } from '../../models/event'; @@ -74,21 +73,19 @@ export class VerificationBase< * *

Subclasses must have a NAME class property.

* - * @class - * - * @param {Object} channel the verification channel to send verification messages over. + * @param channel - the verification channel to send verification messages over. * TODO: Channel types * - * @param {MatrixClient} baseApis base matrix api interface + * @param baseApis - base matrix api interface * - * @param {string} userId the user ID that is being verified + * @param userId - the user ID that is being verified * - * @param {string} deviceId the device ID that is being verified + * @param deviceId - the device ID that is being verified * - * @param {object} [startEvent] the m.key.verification.start event that + * @param startEvent - the m.key.verification.start event that * initiated this verification, if any * - * @param {object} [request] the key verification request object related to + * @param request - the key verification request object related to * this verification, if any */ public constructor( @@ -279,7 +276,7 @@ export class VerificationBase< /** * Begin the key verification * - * @returns {Promise} Promise which resolves when the verification has + * @returns Promise which resolves when the verification has * completed. */ public verify(): Promise { diff --git a/src/crypto/verification/Error.ts b/src/crypto/verification/Error.ts index ca0fb20b575..0ad50b2ff26 100644 --- a/src/crypto/verification/Error.ts +++ b/src/crypto/verification/Error.ts @@ -16,8 +16,6 @@ limitations under the License. /** * Error messages. - * - * @module crypto/verification/Error */ import { MatrixEvent } from "../../models/event"; diff --git a/src/crypto/verification/IllegalMethod.ts b/src/crypto/verification/IllegalMethod.ts index f01364a212f..c437e0cd2a9 100644 --- a/src/crypto/verification/IllegalMethod.ts +++ b/src/crypto/verification/IllegalMethod.ts @@ -17,7 +17,6 @@ limitations under the License. /** * Verification method that is illegal to have (cannot possibly * do verification with this method). - * @module crypto/verification/IllegalMethod */ import { VerificationBase as Base, VerificationEvent, VerificationEventHandlerMap } from "./Base"; @@ -26,10 +25,6 @@ import { MatrixClient } from "../../client"; import { MatrixEvent } from "../../models/event"; import { VerificationRequest } from "./request/VerificationRequest"; -/** - * @class crypto/verification/IllegalMethod/IllegalMethod - * @extends {module:crypto/verification/Base} - */ export class IllegalMethod extends Base { public static factory( channel: IVerificationChannel, diff --git a/src/crypto/verification/QRCode.ts b/src/crypto/verification/QRCode.ts index 13053280d8b..c5c2735bc58 100644 --- a/src/crypto/verification/QRCode.ts +++ b/src/crypto/verification/QRCode.ts @@ -16,7 +16,6 @@ limitations under the License. /** * QR code key verification. - * @module crypto/verification/QRCode */ import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base"; @@ -44,10 +43,6 @@ type EventHandlerMap = { [QrCodeEvent.ShowReciprocateQr]: (qr: IReciprocateQr) => void; } & VerificationEventHandlerMap; -/** - * @class crypto/verification/QRCode/ReciprocateQRCode - * @extends {module:crypto/verification/Base} - */ export class ReciprocateQRCode extends Base { public reciprocateQREvent?: IReciprocateQr; diff --git a/src/crypto/verification/SAS.ts b/src/crypto/verification/SAS.ts index 168a85d28fc..9d831f5c668 100644 --- a/src/crypto/verification/SAS.ts +++ b/src/crypto/verification/SAS.ts @@ -16,7 +16,6 @@ limitations under the License. /** * Short Authentication String (SAS) verification. - * @module crypto/verification/SAS */ import anotherjson from 'another-json'; @@ -232,10 +231,6 @@ type EventHandlerMap = { [SasEvent.ShowSas]: (sas: ISasEvent) => void; } & VerificationEventHandlerMap; -/** - * @alias module:crypto/verification/SAS - * @extends {module:crypto/verification/Base} - */ export class SAS extends Base { private waitingForAccept?: boolean; public ourSASPubKey?: string; diff --git a/src/crypto/verification/SASDecimal.ts b/src/crypto/verification/SASDecimal.ts index c8fa73100e6..26dc8d2a087 100644 --- a/src/crypto/verification/SASDecimal.ts +++ b/src/crypto/verification/SASDecimal.ts @@ -17,11 +17,11 @@ limitations under the License. /** * Implementation of decimal encoding of SAS as per: * https://spec.matrix.org/v1.4/client-server-api/#sas-method-decimal - * @param sasBytes the five bytes generated by HKDF + * @param sasBytes - the five bytes generated by HKDF * @returns the derived three numbers between 1000 and 9191 inclusive */ export function generateDecimalSas(sasBytes: number[]): [number, number, number] { - /** + /* * +--------+--------+--------+--------+--------+ * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | * +--------+--------+--------+--------+--------+ diff --git a/src/crypto/verification/request/InRoomChannel.ts b/src/crypto/verification/request/InRoomChannel.ts index 664a2dad6a8..edbf31f635b 100644 --- a/src/crypto/verification/request/InRoomChannel.ts +++ b/src/crypto/verification/request/InRoomChannel.ts @@ -40,9 +40,9 @@ export class InRoomChannel implements IVerificationChannel { private requestEventId?: string; /** - * @param {MatrixClient} client the matrix client, to send messages with and get current user & device from. - * @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user. - * @param {string} userId id of user that the verification request is directed at, should be present in the room. + * @param client - the matrix client, to send messages with and get current user & device from. + * @param roomId - id of the room where verification events should be posted in, should be a DM with the given user. + * @param userId - id of user that the verification request is directed at, should be present in the room. */ public constructor( private readonly client: MatrixClient, @@ -78,8 +78,8 @@ export class InRoomChannel implements IVerificationChannel { } /** - * @param {MatrixEvent} event the event to get the timestamp of - * @return {number} the timestamp when the event was sent + * @param event - the event to get the timestamp of + * @returns the timestamp when the event was sent */ public getTimestamp(event: MatrixEvent): number { return event.getTs(); @@ -87,8 +87,8 @@ export class InRoomChannel implements IVerificationChannel { /** * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel - * @param {string} type the event type to check - * @returns {boolean} boolean flag + * @param type - the event type to check + * @returns boolean flag */ public static canCreateRequest(type: string): boolean { return type === REQUEST_TYPE; @@ -100,8 +100,8 @@ export class InRoomChannel implements IVerificationChannel { /** * Extract the transaction id used by a given key verification event, if any - * @param {MatrixEvent} event the event - * @returns {string} the transaction id + * @param event - the event + * @returns the transaction id */ public static getTransactionId(event: MatrixEvent): string | undefined { if (InRoomChannel.getEventType(event) === REQUEST_TYPE) { @@ -119,9 +119,9 @@ export class InRoomChannel implements IVerificationChannel { * This only does checks that don't rely on the current state of a potentially already channel * so we can prevent channels being created by invalid events. * `handleEvent` can do more checks and choose to ignore invalid events. - * @param {MatrixEvent} event the event to validate - * @param {MatrixClient} client the client to get the current user and device id from - * @returns {boolean} whether the event is valid and should be passed to handleEvent + * @param event - the event to validate + * @param client - the client to get the current user and device id from + * @returns whether the event is valid and should be passed to handleEvent */ public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean { const txnId = InRoomChannel.getTransactionId(event); @@ -156,8 +156,8 @@ export class InRoomChannel implements IVerificationChannel { * As m.key.verification.request events are as m.room.message events with the InRoomChannel * to have a fallback message in non-supporting clients, we map the real event type * to the symbolic one to keep things in unison with ToDeviceChannel - * @param {MatrixEvent} event the event to get the type of - * @returns {string} the "symbolic" event type + * @param event - the event to get the type of + * @returns the "symbolic" event type */ public static getEventType(event: MatrixEvent): string { const type = event.getType(); @@ -179,10 +179,10 @@ export class InRoomChannel implements IVerificationChannel { /** * Changes the state of the channel, request, and verifier in response to a key verification event. - * @param {MatrixEvent} event to handle - * @param {VerificationRequest} request the request to forward handling to - * @param {boolean} isLiveEvent whether this is an even received through sync or not - * @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent. + * @param event - to handle + * @param request - the request to forward handling to + * @param isLiveEvent - whether this is an even received through sync or not + * @returns a promise that resolves when any requests as an answer to the passed-in event are sent. */ public async handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise { // prevent processing the same event multiple times, as under @@ -228,8 +228,8 @@ export class InRoomChannel implements IVerificationChannel { * so it has the same format as returned by `completeContent` before sending. * The relation can not appear on the event content because of encryption, * relations are excluded from encryption. - * @param {MatrixEvent} event the received event - * @returns {Object} the content object with the relation added again + * @param event - the received event + * @returns the content object with the relation added again */ public completedContentFromEvent(event: MatrixEvent): Record { // ensure m.related_to is included in e2ee rooms @@ -244,9 +244,9 @@ export class InRoomChannel implements IVerificationChannel { * This is public so verification methods (SAS uses this) can get the exact * content that will be sent independent of the used channel, * as they need to calculate the hash of it. - * @param {string} type the event type - * @param {object} content the (incomplete) content - * @returns {object} the complete content, as it will be sent. + * @param type - the event type + * @param content - the (incomplete) content + * @returns the complete content, as it will be sent. */ public completeContent(type: string, content: Record): Record { content = Object.assign({}, content); @@ -276,9 +276,9 @@ export class InRoomChannel implements IVerificationChannel { /** * Send an event over the channel with the content not having gone through `completeContent`. - * @param {string} type the event type - * @param {object} uncompletedContent the (incomplete) content - * @returns {Promise} the promise of the request + * @param type - the event type + * @param uncompletedContent - the (incomplete) content + * @returns the promise of the request */ public send(type: string, uncompletedContent: Record): Promise { const content = this.completeContent(type, uncompletedContent); @@ -287,9 +287,8 @@ export class InRoomChannel implements IVerificationChannel { /** * Send an event over the channel with the content having gone through `completeContent` already. - * @param {string} type the event type - * @param {object} content - * @returns {Promise} the promise of the request + * @param type - the event type + * @returns the promise of the request */ public async sendCompleted(type: string, content: Record): Promise { let sendType = type; diff --git a/src/crypto/verification/request/ToDeviceChannel.ts b/src/crypto/verification/request/ToDeviceChannel.ts index 30d61525139..5d92c3ed12c 100644 --- a/src/crypto/verification/request/ToDeviceChannel.ts +++ b/src/crypto/verification/request/ToDeviceChannel.ts @@ -69,8 +69,8 @@ export class ToDeviceChannel implements IVerificationChannel { /** * Extract the transaction id used by a given key verification event, if any - * @param {MatrixEvent} event the event - * @returns {string} the transaction id + * @param event - the event + * @returns the transaction id */ public static getTransactionId(event: MatrixEvent): string { const content = event.getContent(); @@ -79,8 +79,8 @@ export class ToDeviceChannel implements IVerificationChannel { /** * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel - * @param {string} type the event type to check - * @returns {boolean} boolean flag + * @param type - the event type to check + * @returns boolean flag */ public static canCreateRequest(type: string): boolean { return type === REQUEST_TYPE || type === START_TYPE; @@ -95,9 +95,9 @@ export class ToDeviceChannel implements IVerificationChannel { * This only does checks that don't rely on the current state of a potentially already channel * so we can prevent channels being created by invalid events. * `handleEvent` can do more checks and choose to ignore invalid events. - * @param {MatrixEvent} event the event to validate - * @param {MatrixClient} client the client to get the current user and device id from - * @returns {boolean} whether the event is valid and should be passed to handleEvent + * @param event - the event to validate + * @param client - the client to get the current user and device id from + * @returns whether the event is valid and should be passed to handleEvent */ public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean { if (event.isCancelled()) { @@ -137,8 +137,8 @@ export class ToDeviceChannel implements IVerificationChannel { } /** - * @param {MatrixEvent} event the event to get the timestamp of - * @return {number} the timestamp when the event was sent + * @param event - the event to get the timestamp of + * @returns the timestamp when the event was sent */ public getTimestamp(event: MatrixEvent): number { const content = event.getContent(); @@ -147,10 +147,10 @@ export class ToDeviceChannel implements IVerificationChannel { /** * Changes the state of the channel, request, and verifier in response to a key verification event. - * @param {MatrixEvent} event to handle - * @param {VerificationRequest} request the request to forward handling to - * @param {boolean} isLiveEvent whether this is an even received through sync or not - * @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent. + * @param event - to handle + * @param request - the request to forward handling to + * @param isLiveEvent - whether this is an even received through sync or not + * @returns a promise that resolves when any requests as an answer to the passed-in event are sent. */ public async handleEvent(event: MatrixEvent, request: Request, isLiveEvent = false): Promise { const type = event.getType(); @@ -196,9 +196,9 @@ export class ToDeviceChannel implements IVerificationChannel { } /** - * See {InRoomChannel.completedContentFromEvent} why this is needed. - * @param {MatrixEvent} event the received event - * @returns {Object} the content object + * See {@link InRoomChannel#completedContentFromEvent} for why this is needed. + * @param event - the received event + * @returns the content object */ public completedContentFromEvent(event: MatrixEvent): Record { return event.getContent(); @@ -209,9 +209,9 @@ export class ToDeviceChannel implements IVerificationChannel { * This is public so verification methods (SAS uses this) can get the exact * content that will be sent independent of the used channel, * as they need to calculate the hash of it. - * @param {string} type the event type - * @param {object} content the (incomplete) content - * @returns {object} the complete content, as it will be sent. + * @param type - the event type + * @param content - the (incomplete) content + * @returns the complete content, as it will be sent. */ public completeContent(type: string, content: Record): Record { // make a copy @@ -230,9 +230,9 @@ export class ToDeviceChannel implements IVerificationChannel { /** * Send an event over the channel with the content not having gone through `completeContent`. - * @param {string} type the event type - * @param {object} uncompletedContent the (incomplete) content - * @returns {Promise} the promise of the request + * @param type - the event type + * @param uncompletedContent - the (incomplete) content + * @returns the promise of the request */ public send(type: string, uncompletedContent: Record = {}): Promise { // create transaction id when sending request @@ -245,9 +245,8 @@ export class ToDeviceChannel implements IVerificationChannel { /** * Send an event over the channel with the content having gone through `completeContent` already. - * @param {string} type the event type - * @param {object} content - * @returns {Promise} the promise of the request + * @param type - the event type + * @returns the promise of the request */ public async sendCompleted(type: string, content: Record): Promise { let result; @@ -286,7 +285,7 @@ export class ToDeviceChannel implements IVerificationChannel { /** * Allow Crypto module to create and know the transaction id before the .start event gets sent. - * @returns {string} the transaction id + * @returns the transaction id */ public static makeTransactionId(): string { return randomString(32); diff --git a/src/crypto/verification/request/VerificationRequest.ts b/src/crypto/verification/request/VerificationRequest.ts index 58de4b9a2b0..05f0cd75ca5 100644 --- a/src/crypto/verification/request/VerificationRequest.ts +++ b/src/crypto/verification/request/VerificationRequest.ts @@ -81,6 +81,9 @@ export enum VerificationRequestEvent { } type EventHandlerMap = { + /** + * Fires whenever the state of the request object has changed. + */ [VerificationRequestEvent.Change]: () => void; }; @@ -88,7 +91,6 @@ type EventHandlerMap = { * State machine for verification requests. * Things that differ based on what channel is used to * send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`. - * @event "change" whenever the state of the request object has changed. */ export class VerificationRequest< C extends IVerificationChannel = IVerificationChannel, @@ -129,10 +131,10 @@ export class VerificationRequest< /** * Stateless validation logic not specific to the channel. * Invoked by the same static method in either channel. - * @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel. - * @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead. - * @param {MatrixClient} client the client to get the current user and device id from - * @returns {boolean} whether the event is valid and should be passed to handleEvent + * @param type - the "symbolic" event type, as returned by the `getEventType` function on the channel. + * @param event - the event to validate. Don't call getType() on it but use the `type` parameter instead. + * @param client - the client to get the current user and device id from + * @returns whether the event is valid and should be passed to handleEvent */ public static validateEvent(type: string, event: MatrixEvent, client: MatrixClient): boolean { const content = event.getContent(); @@ -234,7 +236,7 @@ export class VerificationRequest< /** * The key verification request event. - * @returns {MatrixEvent} The request event, or falsey if not found. + * @returns The request event, or falsey if not found. */ public get requestEvent(): MatrixEvent | undefined { return this.getEventByEither(REQUEST_TYPE); @@ -278,9 +280,9 @@ export class VerificationRequest< * This is useful when setting up the QR code UI, as it is somewhat asymmetrical: * if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa. * For methods that need to be supported by both ends, use the `methods` property. - * @param {string} method the method to check - * @param {boolean} force to check even if the phase is not ready or started yet, internal usage - * @return {boolean} whether or not the other party said the supported the method */ + * @param method - the method to check + * @param force - to check even if the phase is not ready or started yet, internal usage + * @returns whether or not the other party said the supported the method */ public otherPartySupportsMethod(method: string, force = false): boolean { if (!force && !this.ready && !this.started) { return false; @@ -398,7 +400,7 @@ export class VerificationRequest< * given the events sent so far in the verification. This is the * same algorithm used to determine which device to send the * verification to when no specific device is specified. - * @returns {{userId: *, deviceId: *}} The device information + * @returns The device information */ public get targetDevice(): ITargetDevice { const theirFirstEvent = @@ -415,10 +417,10 @@ export class VerificationRequest< /* Start the key verification, creating a verifier and sending a .start event. * If no previous events have been sent, pass in `targetDevice` to set who to direct this request to. - * @param {string} method the name of the verification method to use. - * @param {string?} targetDevice.userId the id of the user to direct this request to - * @param {string?} targetDevice.deviceId the id of the device to direct this request to - * @returns {VerifierBase} the verifier of the given method + * @param method - the name of the verification method to use. + * @param targetDevice.userId the id of the user to direct this request to + * @param targetDevice.deviceId the id of the device to direct this request to + * @returns the verifier of the given method */ public beginKeyVerification( method: VerificationMethod, @@ -448,7 +450,7 @@ export class VerificationRequest< /** * sends the initial .request event. - * @returns {Promise} resolves when the event has been sent. + * @returns resolves when the event has been sent. */ public async sendRequest(): Promise { if (!this.observeOnly && this._phase === PHASE_UNSENT) { @@ -459,9 +461,9 @@ export class VerificationRequest< /** * Cancels the request, sending a cancellation to the other party - * @param {string?} error.reason the error reason to send the cancellation with - * @param {string?} error.code the error code to send the cancellation with - * @returns {Promise} resolves when the event has been sent. + * @param reason - the error reason to send the cancellation with + * @param code - the error code to send the cancellation with + * @returns resolves when the event has been sent. */ public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise { if (!this.observeOnly && this._phase !== PHASE_CANCELLED) { @@ -478,7 +480,7 @@ export class VerificationRequest< /** * Accepts the request, sending a .ready event to the other party - * @returns {Promise} resolves when the event has been sent. + * @returns resolves when the event has been sent. */ public async accept(): Promise { if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) { @@ -491,10 +493,10 @@ export class VerificationRequest< /** * Can be used to listen for state changes until the callback returns true. - * @param {Function} fn callback to evaluate whether the request is in the desired state. + * @param fn - callback to evaluate whether the request is in the desired state. * Takes the request as an argument. - * @returns {Promise} that resolves once the callback returns true - * @throws {Error} when the request is cancelled + * @returns that resolves once the callback returns true + * @throws Error when the request is cancelled */ public waitFor(fn: (request: VerificationRequest) => boolean): Promise { return new Promise((resolve, reject) => { @@ -701,13 +703,13 @@ export class VerificationRequest< /** * Changes the state of the request and verifier in response to a key verification event. - * @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel. - * @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead. - * @param {boolean} isLiveEvent whether this is an even received through sync or not - * @param {boolean} isRemoteEcho whether this is the remote echo of an event sent by the same device - * @param {boolean} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers. + * @param type - the "symbolic" event type, as returned by the `getEventType` function on the channel. + * @param event - the event to handle. Don't call getType() on it but use the `type` parameter instead. + * @param isLiveEvent - whether this is an even received through sync or not + * @param isRemoteEcho - whether this is the remote echo of an event sent by the same device + * @param isSentByUs - whether this event is sent by a party that can accept and/or observe the request like one of our peers. * For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device. - * @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent. + * @returns a promise that resolves when any requests as an answer to the passed-in event are sent. */ public async handleEvent( type: string, diff --git a/src/embedded.ts b/src/embedded.ts index 27cf564a670..c5cbab80d14 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -85,7 +85,7 @@ export interface ICapabilities { /** * Whether this client needs access to TURN servers. - * @default false + * @defaultValue false */ turnServers?: boolean; } diff --git a/src/event-mapper.ts b/src/event-mapper.ts index 6f2e25c1bdd..d40f579b5d7 100644 --- a/src/event-mapper.ts +++ b/src/event-mapper.ts @@ -20,8 +20,11 @@ import { IEvent, MatrixEvent, MatrixEventEvent } from "./models/event"; export type EventMapper = (obj: Partial) => MatrixEvent; export interface MapperOpts { + // don't re-emit events emitted on an event mapped by this mapper on the client preventReEmit?: boolean; + // decrypt event proactively decrypt?: boolean; + // the event is a to_device event toDevice?: boolean; } diff --git a/src/filter-component.ts b/src/filter-component.ts index 0bda7d3792d..5a4601258f2 100644 --- a/src/filter-component.ts +++ b/src/filter-component.ts @@ -22,16 +22,12 @@ import { THREAD_RELATION_TYPE, } from "./models/thread"; -/** - * @module filter-component - */ - /** * Checks if a value matches a given field value, which may be a * terminated * wildcard pattern. - * @param {String} actualValue The value to be compared - * @param {String} filterValue The filter pattern to be compared - * @return {boolean} true if the actualValue matches the filterValue + * @param actualValue - The value to be compared + * @param filterValue - The filter pattern to be compared + * @returns true if the actualValue matches the filterValue */ function matchesWildcard(actualValue: string, filterValue: string): boolean { if (filterValue.endsWith("*")) { @@ -68,17 +64,14 @@ export interface IFilterComponent { * * N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as * 'Filters' are referred to as 'FilterCollections'. - * - * @constructor - * @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true } */ export class FilterComponent { public constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {} /** * Checks with the filter component matches the given event - * @param {MatrixEvent} event event to be checked against the filter - * @return {boolean} true if the event matches the filter + * @param event - event to be checked against the filter + * @returns true if the event matches the filter */ public check(event: MatrixEvent): boolean { const bundledRelationships = event.getUnsigned()?.["m.relations"] || {}; @@ -122,13 +115,13 @@ export class FilterComponent { /** * Checks whether the filter component matches the given event fields. - * @param {String} roomId the roomId for the event being checked - * @param {String} sender the sender of the event being checked - * @param {String} eventType the type of the event being checked - * @param {boolean} containsUrl whether the event contains a content.url field - * @param {boolean} relationTypes whether has aggregated relation of the given type - * @param {boolean} relationSenders whether one of the relation is sent by the user listed - * @return {boolean} true if the event fields match the filter + * @param roomId - the roomId for the event being checked + * @param sender - the sender of the event being checked + * @param eventType - the type of the event being checked + * @param containsUrl - whether the event contains a content.url field + * @param relationTypes - whether has aggregated relation of the given type + * @param relationSenders - whether one of the relation is sent by the user listed + * @returns true if the event fields match the filter */ private checkFields( roomId: string | undefined, @@ -194,8 +187,8 @@ export class FilterComponent { /** * Filters a list of events down to those which match this filter component - * @param {MatrixEvent[]} events Events to be checked against the filter component - * @return {MatrixEvent[]} events which matched the filter component + * @param events - Events to be checked against the filter component + * @returns events which matched the filter component */ public filter(events: MatrixEvent[]): MatrixEvent[] { return events.filter(this.check, this); @@ -204,7 +197,7 @@ export class FilterComponent { /** * Returns the limit field for a given filter component, providing a default of * 10 if none is otherwise specified. Cargo-culted from Synapse. - * @return {Number} the limit for this filter component. + * @returns the limit for this filter component. */ public limit(): number { return this.filterJson.limit !== undefined ? this.filterJson.limit : 10; diff --git a/src/filter.ts b/src/filter.ts index 66522701730..b5dc7140bdc 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module filter - */ - import { EventType, RelationType, @@ -27,9 +23,6 @@ import { FilterComponent, IFilterComponent } from "./filter-component"; import { MatrixEvent } from "./models/event"; /** - * @param {Object} obj - * @param {string} keyNesting - * @param {*} val */ function setProp(obj: Record, keyNesting: string, val: any): void { const nestedKeys = keyNesting.split(".") as [keyof typeof obj]; @@ -78,14 +71,6 @@ interface IRoomFilter { } /* eslint-enable camelcase */ -/** - * Construct a new Filter. - * @constructor - * @param {string} userId The user ID for this filter. - * @param {string=} filterId The filter ID if known. - * @prop {string} userId The user ID of the filter - * @prop {?string} filterId The filter ID - */ export class Filter { public static LAZY_LOADING_MESSAGES_FILTER = { lazy_load_members: true, @@ -93,11 +78,6 @@ export class Filter { /** * Create a filter from existing data. - * @static - * @param {string} userId - * @param {string} filterId - * @param {Object} jsonObj - * @return {Filter} */ public static fromJson(userId: string | undefined | null, filterId: string, jsonObj: IFilterDefinition): Filter { const filter = new Filter(userId, filterId); @@ -109,11 +89,16 @@ export class Filter { private roomFilter?: FilterComponent; private roomTimelineFilter?: FilterComponent; + /** + * Construct a new Filter. + * @param userId - The user ID for this filter. + * @param filterId - The filter ID if known. + */ public constructor(public readonly userId: string | undefined | null, public filterId?: string) {} /** * Get the ID of this filter on your homeserver (if known) - * @return {?string} The filter ID + * @returns The filter ID */ public getFilterId(): string | undefined { return this.filterId; @@ -121,7 +106,7 @@ export class Filter { /** * Get the JSON body of the filter. - * @return {Object} The filter definition + * @returns The filter definition */ public getDefinition(): IFilterDefinition { return this.definition; @@ -129,7 +114,7 @@ export class Filter { /** * Set the JSON body of the filter - * @param {Object} definition The filter definition + * @param definition - The filter definition */ public setDefinition(definition: IFilterDefinition): void { this.definition = definition; @@ -198,7 +183,7 @@ export class Filter { /** * Get the room.timeline filter component of the filter - * @return {FilterComponent} room timeline filter component + * @returns room timeline filter component */ public getRoomTimelineFilterComponent(): FilterComponent | undefined { return this.roomTimelineFilter; @@ -207,8 +192,8 @@ export class Filter { /** * Filter the list of events based on whether they are allowed in a timeline * based on this filter - * @param {MatrixEvent[]} events the list of events being filtered - * @return {MatrixEvent[]} the list of events which match the filter + * @param events - the list of events being filtered + * @returns the list of events which match the filter */ public filterRoomTimeline(events: MatrixEvent[]): MatrixEvent[] { if (this.roomFilter) { @@ -222,7 +207,7 @@ export class Filter { /** * Set the max number of events to return for each room's timeline. - * @param {Number} limit The max number of events to return for each room. + * @param limit - The max number of events to return for each room. */ public setTimelineLimit(limit: number): void { setProp(this.definition, "room.timeline.limit", limit); @@ -230,7 +215,6 @@ export class Filter { /** * Enable threads unread notification - * @param {boolean} enabled */ public setUnreadThreadNotifications(enabled: boolean): void { this.definition = { @@ -251,7 +235,7 @@ export class Filter { /** * Control whether left rooms should be included in responses. - * @param {boolean} includeLeave True to make rooms the user has left appear + * @param includeLeave - True to make rooms the user has left appear * in responses. */ public setIncludeLeaveRooms(includeLeave: boolean): void { diff --git a/src/http-api/errors.ts b/src/http-api/errors.ts index 5ae0a0f5099..e48fc029c7d 100644 --- a/src/http-api/errors.ts +++ b/src/http-api/errors.ts @@ -26,9 +26,8 @@ interface IErrorJson extends Partial { /** * Construct a generic HTTP error. This is a JavaScript Error with additional information * specific to HTTP responses. - * @constructor - * @param {string} msg The error message to include. - * @param {number} httpStatus The HTTP response status code. + * @param msg - The error message to include. + * @param httpStatus - The HTTP response status code. */ export class HTTPError extends Error { public constructor(msg: string, public readonly httpStatus?: number) { @@ -36,21 +35,18 @@ export class HTTPError extends Error { } } -/** - * Construct a Matrix error. This is a JavaScript Error with additional - * information specific to the standard Matrix error response. - * @constructor - * @param {Object} errorJson The Matrix error JSON returned from the homeserver. - * @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN". - * @prop {string} name Same as MatrixError.errcode but with a default unknown string. - * @prop {string} message The Matrix 'error' value, e.g. "Missing token." - * @prop {Object} data The raw Matrix error JSON used to construct this object. - * @prop {number} httpStatus The numeric HTTP status code given - */ export class MatrixError extends HTTPError { + // The Matrix 'errcode' value, e.g. "M_FORBIDDEN". public readonly errcode?: string; + // The raw Matrix error JSON used to construct this object. public data: IErrorJson; + /** + * Construct a Matrix error. This is a JavaScript Error with additional + * information specific to the standard Matrix error response. + * @param errorJson - The Matrix error JSON returned from the homeserver. + * @param httpStatus - The numeric HTTP status code given + */ public constructor( errorJson: IErrorJson = {}, public readonly httpStatus?: number, @@ -76,7 +72,6 @@ export class MatrixError extends HTTPError { * that a request failed because of some error with the connection, either * CORS was not correctly configured on the server, the server didn't response, * the request timed out, or the internet connection on the client side went down. - * @constructor */ export class ConnectionError extends Error { public constructor(message: string, cause?: Error) { diff --git a/src/http-api/fetch.ts b/src/http-api/fetch.ts index 71ba098e303..0c966d30d08 100644 --- a/src/http-api/fetch.ts +++ b/src/http-api/fetch.ts @@ -16,7 +16,6 @@ limitations under the License. /** * This is an internal module. See {@link MatrixHttpApi} for the public class. - * @module http-api */ import * as utils from "../utils"; @@ -64,13 +63,13 @@ export class FetchHttpApi { /** * Sets the base URL for the identity server - * @param {string} url The new base url + * @param url - The new base url */ public setIdBaseUrl(url: string): void { this.opts.idBaseUrl = url; } - public idServerRequest( + public idServerRequest>( method: Method, path: string, params: Record | undefined, @@ -104,35 +103,29 @@ export class FetchHttpApi { /** * Perform an authorised request to the homeserver. - * @param {string} method The HTTP method e.g. "GET". - * @param {string} path The HTTP path after the supplied prefix e.g. + * @param method - The HTTP method e.g. "GET". + * @param path - The HTTP path after the supplied prefix e.g. * "/createRoom". * - * @param {Object=} queryParams A dict of query params (these will NOT be + * @param queryParams - A dict of query params (these will NOT be * urlencoded). If unspecified, there will be no query params. * - * @param {Object} [body] The HTTP JSON body. + * @param body - The HTTP JSON body. * - * @param {Object|Number=} opts additional options. If a number is specified, + * @param opts - additional options. If a number is specified, * this is treated as `opts.localTimeoutMs`. * - * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before - * timing out the request. If not specified, there is no timeout. - * - * @param {string=} opts.prefix The full prefix to use e.g. - * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. - * - * @param {string=} opts.baseUrl The alternative base url to use. - * If not specified, uses this.opts.baseUrl - * - * @param {Object=} opts.headers map of additional request headers - * - * @return {Promise} Resolves to {data: {Object}, - * headers: {Object}, code: {Number}}. - * If onlyData is set, this will resolve to the data - * object only. - * @return {module:http-api.MatrixError} Rejects with an error if a problem - * occurred. This includes network problems and Matrix-specific error JSON. + * @returns Promise which resolves to + * ``` + * { + * data: {Object}, + * headers: {Object}, + * code: {Number}, + * } + * ``` + * If `onlyData` is set, this will resolve to the `data` object only. + * @returns Rejects with an error if a problem occurred. + * This includes network problems and Matrix-specific error JSON. */ public authedRequest( method: Method, @@ -176,30 +169,28 @@ export class FetchHttpApi { /** * Perform a request to the homeserver without any credentials. - * @param {string} method The HTTP method e.g. "GET". - * @param {string} path The HTTP path after the supplied prefix e.g. + * @param method - The HTTP method e.g. "GET". + * @param path - The HTTP path after the supplied prefix e.g. * "/createRoom". * - * @param {Object=} queryParams A dict of query params (these will NOT be + * @param queryParams - A dict of query params (these will NOT be * urlencoded). If unspecified, there will be no query params. * - * @param {Object} [body] The HTTP JSON body. + * @param body - The HTTP JSON body. * - * @param {Object=} opts additional options + * @param opts - additional options * - * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before - * timing out the request. If not specified, there is no timeout. - * - * @param {string=} opts.prefix The full prefix to use e.g. - * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. - * - * @param {Object=} opts.headers map of additional request headers - * - * @return {Promise} Resolves to {data: {Object}, - * headers: {Object}, code: {Number}}. - * If onlyData is set, this will resolve to the data + * @returns Promise which resolves to + * ``` + * { + * data: {Object}, + * headers: {Object}, + * code: {Number}, + * } + * ``` + * If `onlyData is set, this will resolve to the data` * object only. - * @return {module:http-api.MatrixError} Rejects with an error if a problem + * @returns Rejects with an error if a problem * occurred. This includes network problems and Matrix-specific error JSON. */ public request( @@ -215,21 +206,16 @@ export class FetchHttpApi { /** * Perform a request to an arbitrary URL. - * @param {string} method The HTTP method e.g. "GET". - * @param {string} url The HTTP URL object. - * - * @param {Object} [body] The HTTP JSON body. - * - * @param {Object=} opts additional options + * @param method - The HTTP method e.g. "GET". + * @param url - The HTTP URL object. * - * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before - * timing out the request. If not specified, there is no timeout. + * @param body - The HTTP JSON body. * - * @param {Object=} opts.headers map of additional request headers + * @param opts - additional options * - * @return {Promise} Resolves to data unless `onlyData` is specified as false, + * @returns Promise which resolves to data unless `onlyData` is specified as false, * where the resolved value will be a fetch Response object. - * @return {module:http-api.MatrixError} Rejects with an error if a problem + * @returns Rejects with an error if a problem * occurred. This includes network problems and Matrix-specific error JSON. */ public async requestOtherUrl( @@ -310,11 +296,11 @@ export class FetchHttpApi { /** * Form and return a homeserver request URL based on the given path params and prefix. - * @param {string} path The HTTP path after the supplied prefix e.g. "/createRoom". - * @param {Object} queryParams A dict of query params (these will NOT be urlencoded). - * @param {string} prefix The full prefix to use e.g. "/_matrix/client/v2_alpha", defaulting to this.opts.prefix. - * @param {string} baseUrl The baseUrl to use e.g. "https://matrix.org/", defaulting to this.opts.baseUrl. - * @return {string} URL + * @param path - The HTTP path after the supplied prefix e.g. "/createRoom". + * @param queryParams - A dict of query params (these will NOT be urlencoded). + * @param prefix - The full prefix to use e.g. "/_matrix/client/v2_alpha", defaulting to this.opts.prefix. + * @param baseUrl - The baseUrl to use e.g. "https://matrix.org/", defaulting to this.opts.baseUrl. + * @returns URL */ public getUrl( path: string, diff --git a/src/http-api/index.ts b/src/http-api/index.ts index c7f782d8972..3574f539e7f 100644 --- a/src/http-api/index.ts +++ b/src/http-api/index.ts @@ -35,27 +35,13 @@ export class MatrixHttpApi extends FetchHttpApi { /** * Upload content to the homeserver * - * @param {object} file The object to upload. On a browser, something that + * @param file - The object to upload. On a browser, something that * can be sent to XMLHttpRequest.send (typically a File). Under node.js, * a Buffer, String or ReadStream. * - * @param {object} opts options object + * @param opts - options object * - * @param {string=} opts.name Name to give the file on the server. Defaults - * to file.name. - * - * @param {boolean=} opts.includeFilename if false will not send the filename, - * e.g for encrypted file uploads where filename leaks are undesirable. - * Defaults to true. - * - * @param {string=} opts.type Content-type for the upload. Defaults to - * file.type, or application/octet-stream. - * - * @param {Function=} opts.progressHandler Optional. Called when a chunk of - * data has been uploaded, with an object containing the fields `loaded` - * (number of bytes transferred) and `total` (total size, if known). - * - * @return {Promise} Resolves to response object, as + * @returns Promise which resolves to response object, as * determined by this.opts.onlyData, opts.rawResponse, and * opts.onlyContentUri. Rejects with an error (usually a MatrixError). */ @@ -190,7 +176,7 @@ export class MatrixHttpApi extends FetchHttpApi { /** * Get the content repository url with query parameters. - * @return {Object} An object with a 'base', 'path' and 'params' for base URL, + * @returns An object with a 'base', 'path' and 'params' for base URL, * path and query parameters respectively. */ public getContentUri(): IContentUri { diff --git a/src/http-api/interface.ts b/src/http-api/interface.ts index 37217947027..9946aa37bf2 100644 --- a/src/http-api/interface.ts +++ b/src/http-api/interface.ts @@ -32,11 +32,25 @@ export interface IHttpOpts { } export interface IRequestOpts { + /** + * The alternative base url to use. + * If not specified, uses this.opts.baseUrl + */ baseUrl?: string; + /** + * The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + */ prefix?: string; - + /** + * map of additional request headers + */ headers?: Record; abortSignal?: AbortSignal; + /** + * The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + */ localTimeoutMs?: number; keepAlive?: boolean; // defaults to false json?: boolean; // defaults to true @@ -62,7 +76,29 @@ export enum HttpApiEvent { } export type HttpApiEventHandlerMap = { + /** + * Fires whenever the login session the JS SDK is using is no + * longer valid and the user must log in again. + * NB. This only fires when action is required from the user, not + * when then login session can be renewed by using a refresh token. + * @example + * ``` + * matrixClient.on("Session.logged_out", function(errorObj){ + * // show the login screen + * }); + * ``` + */ [HttpApiEvent.SessionLoggedOut]: (err: MatrixError) => void; + /** + * Fires when the JS SDK receives a M_CONSENT_NOT_GIVEN error in response + * to a HTTP request. + * @example + * ``` + * matrixClient.on("no_consent", function(message, contentUri) { + * console.info(message + ' Go to ' + contentUri); + * }); + * ``` + */ [HttpApiEvent.NoConsent]: (message: string, consentUri: string) => void; }; @@ -72,9 +108,26 @@ export interface UploadProgress { } export interface UploadOpts { + /** + * Name to give the file on the server. Defaults to file.name. + */ name?: string; + /** + * Content-type for the upload. Defaults to + * file.type, or applicaton/octet-stream. + */ type?: string; + /** + * if false will not send the filename, + * e.g for encrypted file uploads where filename leaks are undesirable. + * Defaults to true. + */ includeFilename?: boolean; + /** + * Optional. Called when a chunk of + * data has been uploaded, with an object containing the fields `loaded` + * (number of bytes transferred) and `total` (total size, if known). + */ progressHandler?(progress: UploadProgress): void; abortController?: AbortController; } diff --git a/src/http-api/utils.ts b/src/http-api/utils.ts index 50466095002..c49be740ef6 100644 --- a/src/http-api/utils.ts +++ b/src/http-api/utils.ts @@ -67,9 +67,9 @@ export function anySignal(signals: AbortSignal[]): { * If it is a JSON response, we will parse it into a MatrixError. Otherwise * we return a generic Error. * - * @param {XMLHttpRequest|Response} response response object - * @param {String} body raw body of the response - * @returns {Error} + * @param response - response object + * @param body - raw body of the response + * @returns */ export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error { let contentType: ParsedMediaType | null; @@ -102,8 +102,8 @@ function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest * * returns null if no content-type header could be found. * - * @param {XMLHttpRequest|Response} response response object - * @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found + * @param response - response object + * @returns parsed content-type header, or null if not found */ function getResponseContentType(response: XMLHttpRequest | Response): ParsedMediaType | null { let contentType: string | null; @@ -124,10 +124,10 @@ function getResponseContentType(response: XMLHttpRequest | Response): ParsedMedi /** * Retries a network operation run in a callback. - * @param {number} maxAttempts maximum attempts to try - * @param {Function} callback callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again. - * @return {any} the result of the network operation - * @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError + * @param maxAttempts - maximum attempts to try + * @param callback - callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again. + * @returns the result of the network operation + * @throws {@link ConnectionError} If after maxAttempts the callback still throws ConnectionError */ export async function retryNetworkOperation(maxAttempts: number, callback: () => Promise): Promise { let attempts = 0; diff --git a/src/indexeddb-helpers.ts b/src/indexeddb-helpers.ts index 695a3145ff2..6f99ae54b8b 100644 --- a/src/indexeddb-helpers.ts +++ b/src/indexeddb-helpers.ts @@ -18,9 +18,9 @@ limitations under the License. * Check if an IndexedDB database exists. The only way to do so is to try opening it, so * we do that and then delete it did not exist before. * - * @param {Object} indexedDB The `indexedDB` interface - * @param {string} dbName The database name to test for - * @returns {boolean} Whether the database exists + * @param indexedDB - The `indexedDB` interface + * @param dbName - The database name to test for + * @returns Whether the database exists */ export function exists(indexedDB: IDBFactory, dbName: string): Promise { return new Promise((resolve, reject) => { diff --git a/src/indexeddb-worker.ts b/src/indexeddb-worker.ts index 45facc485a4..78e87b37329 100644 --- a/src/indexeddb-worker.ts +++ b/src/indexeddb-worker.ts @@ -20,6 +20,6 @@ limitations under the License. * to be used separately */ -/** The {@link module:indexeddb-store-worker~IndexedDBStoreWorker} class. */ +/** The {@link IndexedDBStoreWorker} class. */ export { IndexedDBStoreWorker } from "./store/indexeddb-store-worker"; diff --git a/src/interactive-auth.ts b/src/interactive-auth.ts index df4a42c2726..219569e6148 100644 --- a/src/interactive-auth.ts +++ b/src/interactive-auth.ts @@ -16,8 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** @module interactive-auth */ - import { logger } from './logger'; import { MatrixClient } from "./client"; import { defer, IDeferred } from "./utils"; @@ -31,8 +29,11 @@ interface IFlow { } export interface IInputs { + // An email address. If supplied, a flow using email verification will be chosen. emailAddress?: string; + // An ISO two letter country code. Gives the country that opts.phoneNumber should be resolved relative to. phoneCountry?: string; + // A phone number. If supplied, a flow using phone number validation will be chosen. phoneNumber?: string; registrationToken?: string; } @@ -106,15 +107,66 @@ class NoAuthFlowFoundError extends Error { } interface IOpts { + /** + * A matrix client to use for the auth process + */ matrixClient: MatrixClient; + /** + * Error response from the last request. If null, a request will be made with no auth before starting. + */ authData?: IAuthData; + /** + * Inputs provided by the user and used by different stages of the auto process. + * The inputs provided will affect what flow is chosen. + */ inputs?: IInputs; + /** + * If resuming an existing interactive auth session, the sessionId of that session. + */ sessionId?: string; + /** + * If resuming an existing interactive auth session, the client secret for that session + */ clientSecret?: string; + /** + * If returning from having completed m.login.email.identity auth, the sid for the email verification session. + */ emailSid?: string; + + /** + * Called with the new auth dict to submit the request. + * Also passes a second deprecated arg which is a flag set to true if this request is a background request. + * The busyChanged callback should be used instead of the background flag. + * Should return a promise which resolves to the successful response or rejects with a MatrixError. + */ doRequest(auth: IAuthData | null, background: boolean): Promise; + /** + * Called when the status of the UI auth changes, + * ie. when the state of an auth stage changes of when the auth flow moves to a new stage. + * The arguments are: the login type (eg m.login.password); and an object which is either an error or an + * informational object specific to the login type. + * If the 'errcode' key is defined, the object is an error, and has keys: + * errcode: string, the textual error code, eg. M_UNKNOWN + * error: string, human readable string describing the error + * + * The login type specific objects are as follows: + * m.login.email.identity: + * * emailSid: string, the sid of the active email auth session + */ stateUpdated(nextStage: AuthType, status: IStageStatus): void; + + /** + * A function that takes the email address (string), clientSecret (string), attempt number (int) and + * sessionId (string) and calls the relevant requestToken function and returns the promise returned by that + * function. + * If the resulting promise rejects, the rejection will propagate through to the attemptAuth promise. + */ requestEmailToken(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>; + /** + * Called whenever the interactive auth logic becomes busy submitting information provided by the user or finishes. + * After this has been called with true the UI should indicate that a request is in progress + * until it is called again with false. + */ busyChanged?(busy: boolean): void; startAuthStage?(nextStage: string): Promise; // LEGACY } @@ -131,70 +183,7 @@ interface IOpts { * callbacks, and information gathered from the user can be submitted with * submitAuthDict. * - * @constructor - * @alias module:interactive-auth - * - * @param {object} opts options object - * - * @param {object} opts.matrixClient A matrix client to use for the auth process - * - * @param {object?} opts.authData error response from the last request. If - * null, a request will be made with no auth before starting. - * - * @param {function(object?): Promise} opts.doRequest - * called with the new auth dict to submit the request. Also passes a - * second deprecated arg which is a flag set to true if this request - * is a background request. The busyChanged callback should be used - * instead of the background flag. Should return a promise which resolves - * to the successful response or rejects with a MatrixError. - * - * @param {function(boolean): Promise} opts.busyChanged - * called whenever the interactive auth logic becomes busy submitting - * information provided by the user or finishes. After this has been - * called with true the UI should indicate that a request is in progress - * until it is called again with false. - * - * @param {function(string, object?)} opts.stateUpdated - * called when the status of the UI auth changes, ie. when the state of - * an auth stage changes of when the auth flow moves to a new stage. - * The arguments are: the login type (eg m.login.password); and an object - * which is either an error or an informational object specific to the - * login type. If the 'errcode' key is defined, the object is an error, - * and has keys: - * errcode: string, the textual error code, eg. M_UNKNOWN - * error: string, human readable string describing the error - * - * The login type specific objects are as follows: - * m.login.email.identity: - * * emailSid: string, the sid of the active email auth session - * - * @param {object?} opts.inputs Inputs provided by the user and used by different - * stages of the auto process. The inputs provided will affect what flow is chosen. - * - * @param {string?} opts.inputs.emailAddress An email address. If supplied, a flow - * using email verification will be chosen. - * - * @param {string?} opts.inputs.phoneCountry An ISO two letter country code. Gives - * the country that opts.phoneNumber should be resolved relative to. - * - * @param {string?} opts.inputs.phoneNumber A phone number. If supplied, a flow - * using phone number validation will be chosen. - * - * @param {string?} opts.sessionId If resuming an existing interactive auth session, - * the sessionId of that session. - * - * @param {string?} opts.clientSecret If resuming an existing interactive auth session, - * the client secret for that session - * - * @param {string?} opts.emailSid If returning from having completed m.login.email.identity - * auth, the sid for the email verification session. - * - * @param {function?} opts.requestEmailToken A function that takes the email address (string), - * clientSecret (string), attempt number (int) and sessionId (string) and calls the - * relevant requestToken function and returns the promise returned by that function. - * If the resulting promise rejects, the rejection will propagate through to the - * attemptAuth promise. - * + * @param opts - options object */ export class InteractiveAuth { private readonly matrixClient: MatrixClient; @@ -236,7 +225,7 @@ export class InteractiveAuth { /** * begin the authentication process. * - * @return {Promise} which resolves to the response on success, + * @returns which resolves to the response on success, * or rejects with the error on failure. Rejects with NoAuthFlowFoundError if * no suitable authentication flow can be found */ @@ -307,7 +296,7 @@ export class InteractiveAuth { /** * get the auth session ID * - * @return {string} session id + * @returns session id */ public getSessionId(): string | undefined { return this.data?.session; @@ -317,7 +306,7 @@ export class InteractiveAuth { * get the client secret used for validation sessions * with the identity server. * - * @return {string} client secret + * @returns client secret */ public getClientSecret(): string { return this.clientSecret; @@ -326,8 +315,8 @@ export class InteractiveAuth { /** * get the server params for a given stage * - * @param {string} loginType login type for the stage - * @return {object?} any parameters from the server for this stage + * @param loginType - login type for the stage + * @returns any parameters from the server for this stage */ public getStageParams(loginType: string): Record | undefined { return this.data.params?.[loginType]; @@ -342,10 +331,10 @@ export class InteractiveAuth { * make attemptAuth resolve/reject, or cause the startAuthStage callback * to be called for a new stage. * - * @param {object} authData new auth dict to send to the server. Should + * @param authData - new auth dict to send to the server. Should * include a `type` property denoting the login type, as well as any * other params for that stage. - * @param {boolean} background If true, this request failing will not result + * @param background - If true, this request failing will not result * in the attemptAuth promise being rejected. This can be set to true * for requests that just poll to see if auth has been completed elsewhere. */ @@ -398,7 +387,7 @@ export class InteractiveAuth { * Gets the sid for the email validation session * Specific to m.login.email.identity * - * @returns {string} The sid of the email auth session + * @returns The sid of the email auth session */ public getEmailSid(): string | undefined { return this.emailSid; @@ -410,7 +399,7 @@ export class InteractiveAuth { * of the email validation. * Specific to m.login.email.identity * - * @param {string} sid The sid for the email validation session + * @param sid - The sid for the email validation session */ public setEmailSid(sid: string): void { this.emailSid = sid; @@ -448,9 +437,9 @@ export class InteractiveAuth { * Fire off a request, and either resolve the promise, or call * startAuthStage. * - * @private - * @param {object?} auth new auth dict, including session id - * @param {boolean?} background If true, this request is a background poll, so it + * @internal + * @param auth - new auth dict, including session id + * @param background - If true, this request is a background poll, so it * failing will not result in the attemptAuth promise being rejected. * This can be set to true for requests that just poll to see if auth has * been completed elsewhere. @@ -526,8 +515,8 @@ export class InteractiveAuth { /** * Pick the next stage and call the callback * - * @private - * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + * @internal + * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found */ private startNextAuthStage(): void { const nextStage = this.chooseStage(); @@ -559,9 +548,9 @@ export class InteractiveAuth { /** * Pick the next auth stage * - * @private - * @return {string?} login type - * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + * @internal + * @returns login type + * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found */ private chooseStage(): AuthType | undefined { if (this.chosenFlow === null) { @@ -584,9 +573,9 @@ export class InteractiveAuth { * this could result in the email not being used which would leave * the account with no means to reset a password. * - * @private - * @return {object} flow - * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + * @internal + * @returns flow + * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found */ private chooseFlow(): IFlow { const flows = this.data.flows || []; @@ -625,9 +614,8 @@ export class InteractiveAuth { /** * Get the first uncompleted stage in the given flow * - * @private - * @param {object} flow - * @return {string} login type + * @internal + * @returns login type */ private firstUncompletedStage(flow: IFlow): AuthType | undefined { const completed = this.data.completed || []; diff --git a/src/logger.ts b/src/logger.ts index de1ca6619c1..7077dee02e2 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -15,10 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module logger - */ - import log, { Logger } from "loglevel"; // This is to demonstrate, that you can use any namespace you want. @@ -56,7 +52,7 @@ log.methodFactory = function(methodName, logLevel, loggerName) { }; /** - * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. + * Drop-in replacement for `console` using {@link https://www.npmjs.com/package/loglevel|loglevel}. * Can be tailored down to specific use cases if needed. */ export const logger = log.getLogger(DEFAULT_NAMESPACE) as PrefixedLogger; diff --git a/src/matrix.ts b/src/matrix.ts index 421e0e6ed87..d075c098e64 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -70,8 +70,7 @@ let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore; /** * Configure a different factory to be used for creating crypto stores * - * @param {Function} fac a function which will return a new - * {@link module:crypto.store.base~CryptoStore}. + * @param fac - a function which will return a new {@link CryptoStore} */ export function setCryptoStoreFactory(fac: () => CryptoStore): void { cryptoStoreFactory = fac; @@ -88,24 +87,14 @@ function amendClientOpts(opts: ICreateClientOpts): ICreateClientOpts { } /** - * Construct a Matrix Client. Similar to {@link module:client.MatrixClient} + * Construct a Matrix Client. Similar to {@link MatrixClient} * except that the 'request', 'store' and 'scheduler' dependencies are satisfied. - * @param {Object} opts The configuration options for this client. These configuration - * options will be passed directly to {@link module:client.MatrixClient}. - * @param {Object} opts.store If not set, defaults to - * {@link module:store/memory.MemoryStore}. - * @param {Object} opts.scheduler If not set, defaults to - * {@link module:scheduler~MatrixScheduler}. + * @param opts - The configuration options for this client. These configuration + * options will be passed directly to {@link MatrixClient}. * - * @param {module:crypto.store.base~CryptoStore=} opts.cryptoStore - * crypto store implementation. Calls the factory supplied to - * {@link setCryptoStoreFactory} if unspecified; or if no factory has been - * specified, uses a default implementation (indexeddb in the browser, - * in-memory otherwise). - * - * @return {MatrixClient} A new matrix client. - * @see {@link module:client.MatrixClient} for the full list of options for - * opts. + * @returns A new matrix client. + * @see {@link MatrixClient} for the full list of options for + * `opts`. */ export function createClient(opts: ICreateClientOpts): MatrixClient { return new MatrixClient(amendClientOpts(opts)); diff --git a/src/models/MSC3089Branch.ts b/src/models/MSC3089Branch.ts index 25ce51a20a1..e4c0eb2870a 100644 --- a/src/models/MSC3089Branch.ts +++ b/src/models/MSC3089Branch.ts @@ -67,7 +67,7 @@ export class MSC3089Branch { /** * Deletes the file from the tree, including all prior edits/versions. - * @returns {Promise} Resolves when complete. + * @returns Promise which resolves when complete. */ public async delete(): Promise { await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {}, this.id); @@ -79,7 +79,7 @@ export class MSC3089Branch { /** * Gets the name for this file. - * @returns {string} The name, or "Unnamed File" if unknown. + * @returns The name, or "Unnamed File" if unknown. */ public getName(): string { return this.indexEvent.getContent()['name'] || "Unnamed File"; @@ -87,8 +87,8 @@ export class MSC3089Branch { /** * Sets the name for this file. - * @param {string} name The new name for this file. - * @returns {Promise} Resolves when complete. + * @param name - The new name for this file. + * @returns Promise which resolves when complete. */ public async setName(name: string): Promise { await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, { @@ -99,7 +99,7 @@ export class MSC3089Branch { /** * Gets whether or not a file is locked. - * @returns {boolean} True if locked, false otherwise. + * @returns True if locked, false otherwise. */ public isLocked(): boolean { return this.indexEvent.getContent()['locked'] || false; @@ -107,8 +107,8 @@ export class MSC3089Branch { /** * Sets a file as locked or unlocked. - * @param {boolean} locked True to lock the file, false otherwise. - * @returns {Promise} Resolves when complete. + * @param locked - True to lock the file, false otherwise. + * @returns Promise which resolves when complete. */ public async setLocked(locked: boolean): Promise { await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, { @@ -119,7 +119,7 @@ export class MSC3089Branch { /** * Gets information about the file needed to download it. - * @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file. + * @returns Information about the file. */ public async getFileInfo(): Promise<{ info: IEncryptedFile, httpUrl: string }> { const event = await this.getFileEvent(); @@ -136,7 +136,7 @@ export class MSC3089Branch { /** * Gets the event the file points to. - * @returns {Promise} Resolves to the file's event. + * @returns Promise which resolves to the file's event. */ public async getFileEvent(): Promise { const room = this.client.getRoom(this.roomId); @@ -161,11 +161,11 @@ export class MSC3089Branch { /** * Creates a new version of this file with contents in a type that is compatible with MatrixClient.uploadContent(). - * @param {string} name The name of the file. - * @param {File | String | Buffer | ReadStream | Blob} encryptedContents The encrypted contents. - * @param {Partial} info The encrypted file information. - * @param {IContent} additionalContent Optional event content fields to include in the message. - * @returns {Promise} Resolves to the file event's sent response. + * @param name - The name of the file. + * @param encryptedContents - The encrypted contents. + * @param info - The encrypted file information. + * @param additionalContent - Optional event content fields to include in the message. + * @returns Promise which resolves to the file event's sent response. */ public async createNewVersion( name: string, @@ -200,7 +200,7 @@ export class MSC3089Branch { /** * Gets the file's version history, starting at this file. - * @returns {Promise} Resolves to the file's version history, with the + * @returns Promise which resolves to the file's version history, with the * first element being the current version and the last element being the first version. */ public async getVersionHistory(): Promise { diff --git a/src/models/MSC3089TreeSpace.ts b/src/models/MSC3089TreeSpace.ts index f437eab84da..76c733996c1 100644 --- a/src/models/MSC3089TreeSpace.ts +++ b/src/models/MSC3089TreeSpace.ts @@ -111,8 +111,8 @@ export class MSC3089TreeSpace { /** * Sets the name of the tree space. - * @param {string} name The new name for the space. - * @returns {Promise} Resolves when complete. + * @param name - The new name for the space. + * @returns Promise which resolves when complete. */ public async setName(name: string): Promise { await this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, ""); @@ -121,15 +121,15 @@ export class MSC3089TreeSpace { /** * Invites a user to the tree space. They will be given the default Viewer * permission level unless specified elsewhere. - * @param {string} userId The user ID to invite. - * @param {boolean} andSubspaces True (default) to invite the user to all + * @param userId - The user ID to invite. + * @param andSubspaces - True (default) to invite the user to all * directories/subspaces too, recursively. - * @param {boolean} shareHistoryKeys True (default) to share encryption keys + * @param shareHistoryKeys - True (default) to share encryption keys * with the invited user. This will allow them to decrypt the events (files) * in the tree. Keys will not be shared if the room is lacking appropriate * history visibility (by default, history visibility is "shared" in trees, * which is an appropriate visibility for these purposes). - * @returns {Promise} Resolves when complete. + * @returns Promise which resolves when complete. */ public async invite(userId: string, andSubspaces = true, shareHistoryKeys = true): Promise { const promises: Promise[] = [this.retryInvite(userId)]; @@ -164,9 +164,9 @@ export class MSC3089TreeSpace { * Sets the permissions of a user to the given role. Note that if setting a user * to Owner then they will NOT be able to be demoted. If the user does not have * permission to change the power level of the target, an error will be thrown. - * @param {string} userId The user ID to change the role of. - * @param {TreePermissions} role The role to assign. - * @returns {Promise} Resolves when complete. + * @param userId - The user ID to change the role of. + * @param role - The role to assign. + * @returns Promise which resolves when complete. */ public async setPermissions(userId: string, role: TreePermissions): Promise { const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); @@ -200,8 +200,8 @@ export class MSC3089TreeSpace { * Gets the current permissions of a user. Note that any users missing explicit permissions (or not * in the space) will be considered Viewers. Appropriate membership checks need to be performed * elsewhere. - * @param {string} userId The user ID to check permissions of. - * @returns {TreePermissions} The permissions for the user, defaulting to Viewer. + * @param userId - The user ID to check permissions of. + * @returns The permissions for the user, defaulting to Viewer. */ public getPermissions(userId: string): TreePermissions { const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); @@ -220,8 +220,8 @@ export class MSC3089TreeSpace { /** * Creates a directory under this tree space, represented as another tree space. - * @param {string} name The name for the directory. - * @returns {Promise} Resolves to the created directory. + * @param name - The name for the directory. + * @returns Promise which resolves to the created directory. */ public async createDirectory(name: string): Promise { const directory = await this.client.unstableCreateFileTree(name); @@ -239,7 +239,7 @@ export class MSC3089TreeSpace { /** * Gets a list of all known immediate subdirectories to this tree space. - * @returns {MSC3089TreeSpace[]} The tree spaces (directories). May be empty, but not null. + * @returns The tree spaces (directories). May be empty, but not null. */ public getDirectories(): MSC3089TreeSpace[] { const trees: MSC3089TreeSpace[] = []; @@ -261,8 +261,8 @@ export class MSC3089TreeSpace { /** * Gets a subdirectory of a given ID under this tree space. Note that this will not recurse * into children and instead only look one level deep. - * @param {string} roomId The room ID (directory ID) to find. - * @returns {MSC3089TreeSpace | undefined} The directory, or undefined if not found. + * @param roomId - The room ID (directory ID) to find. + * @returns The directory, or undefined if not found. */ public getDirectory(roomId: string): MSC3089TreeSpace | undefined { return this.getDirectories().find(r => r.roomId === roomId); @@ -270,7 +270,7 @@ export class MSC3089TreeSpace { /** * Deletes the tree, kicking all members and deleting **all subdirectories**. - * @returns {Promise} Resolves when complete. + * @returns Promise which resolves when complete. */ public async delete(): Promise { const subdirectories = this.getDirectories(); @@ -341,7 +341,7 @@ export class MSC3089TreeSpace { /** * Gets the current order index for this directory. Note that if this is the top level space * then -1 will be returned. - * @returns {number} The order index of this space. + * @returns The order index of this space. */ public getOrder(): number { if (this.isTopLevel) return -1; @@ -357,8 +357,8 @@ export class MSC3089TreeSpace { * Sets the order index for this directory within its parent. Note that if this is a top level * space then an error will be thrown. -1 can be used to move the child to the start, and numbers * larger than the number of children can be used to move the child to the end. - * @param {number} index The new order index for this space. - * @returns {Promise} Resolves when complete. + * @param index - The new order index for this space. + * @returns Promise which resolves when complete. * @throws Throws if this is a top level space. */ public async setOrder(index: number): Promise { @@ -464,11 +464,11 @@ export class MSC3089TreeSpace { /** * Creates (uploads) a new file to this tree. The file must have already been encrypted for the room. * The file contents are in a type that is compatible with MatrixClient.uploadContent(). - * @param {string} name The name of the file. - * @param {File | String | Buffer | ReadStream | Blob} encryptedContents The encrypted contents. - * @param {Partial} info The encrypted file information. - * @param {IContent} additionalContent Optional event content fields to include in the message. - * @returns {Promise} Resolves to the file event's sent response. + * @param name - The name of the file. + * @param encryptedContents - The encrypted contents. + * @param info - The encrypted file information. + * @param additionalContent - Optional event content fields to include in the message. + * @returns Promise which resolves to the file event's sent response. */ public async createFile( name: string, @@ -512,8 +512,8 @@ export class MSC3089TreeSpace { /** * Retrieves a file from the tree. - * @param {string} fileEventId The event ID of the file. - * @returns {MSC3089Branch | null} The file, or null if not found. + * @param fileEventId - The event ID of the file. + * @returns The file, or null if not found. */ public getFile(fileEventId: string): MSC3089Branch | null { const branch = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name, fileEventId); @@ -522,7 +522,7 @@ export class MSC3089TreeSpace { /** * Gets an array of all known files for the tree. - * @returns {MSC3089Branch[]} The known files. May be empty, but not null. + * @returns The known files. May be empty, but not null. */ public listFiles(): MSC3089Branch[] { return this.listAllFiles().filter(b => b.isActive); @@ -530,7 +530,7 @@ export class MSC3089TreeSpace { /** * Gets an array of all known files for the tree, including inactive/invalid ones. - * @returns {MSC3089Branch[]} The known files. May be empty, but not null. + * @returns The known files. May be empty, but not null. */ public listAllFiles(): MSC3089Branch[] { const branches = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name) ?? []; diff --git a/src/models/event-context.ts b/src/models/event-context.ts index 60252627bde..0401cd53007 100644 --- a/src/models/event-context.ts +++ b/src/models/event-context.ts @@ -17,9 +17,6 @@ limitations under the License. import { MatrixEvent } from "./event"; import { Direction } from "./event-timeline"; -/** - * @module models/event-context - */ export class EventContext { private timeline: MatrixEvent[]; private ourEventIndex = 0; @@ -38,9 +35,7 @@ export class EventContext { * It also stores pagination tokens for going backwards and forwards in the * timeline. * - * @param {MatrixEvent} ourEvent the event at the centre of this context - * - * @constructor + * @param ourEvent - the event at the centre of this context */ public constructor(public readonly ourEvent: MatrixEvent) { this.timeline = [ourEvent]; @@ -51,7 +46,7 @@ export class EventContext { * * This is a convenience function for getTimeline()[getOurEventIndex()]. * - * @return {MatrixEvent} The event at the centre of this context. + * @returns The event at the centre of this context. */ public getEvent(): MatrixEvent { return this.timeline[this.ourEventIndex]; @@ -60,7 +55,7 @@ export class EventContext { /** * Get the list of events in this context * - * @return {Array} An array of MatrixEvents + * @returns An array of MatrixEvents */ public getTimeline(): MatrixEvent[] { return this.timeline; @@ -68,8 +63,6 @@ export class EventContext { /** * Get the index in the timeline of our event - * - * @return {Number} */ public getOurEventIndex(): number { return this.ourEventIndex; @@ -78,9 +71,7 @@ export class EventContext { /** * Get a pagination token. * - * @param {boolean} backwards true to get the pagination token for going - * backwards in time - * @return {string} + * @param backwards - true to get the pagination token for going */ public getPaginateToken(backwards = false): string | null { return this.paginateTokens[backwards ? Direction.Backward : Direction.Forward]; @@ -91,8 +82,8 @@ export class EventContext { * * Generally this will be used only by the matrix js sdk. * - * @param {string} token pagination token - * @param {boolean} backwards true to set the pagination token for going + * @param token - pagination token + * @param backwards - true to set the pagination token for going * backwards in time */ public setPaginateToken(token?: string, backwards = false): void { @@ -102,8 +93,8 @@ export class EventContext { /** * Add more events to the timeline * - * @param {Array} events new events, in timeline order - * @param {boolean} atStart true to insert new events at the start + * @param events - new events, in timeline order + * @param atStart - true to insert new events at the start */ public addEvents(events: MatrixEvent[], atStart = false): void { // TODO: should we share logic with Room.addEventsToTimeline? diff --git a/src/models/event-status.ts b/src/models/event-status.ts index faca97186c9..a5113e0b790 100644 --- a/src/models/event-status.ts +++ b/src/models/event-status.ts @@ -17,7 +17,6 @@ limitations under the License. /** * Enum for event statuses. * @readonly - * @enum {string} */ export enum EventStatus { /** The event was not sent and will no longer be retried. */ diff --git a/src/models/event-timeline-set.ts b/src/models/event-timeline-set.ts index 6dd2a0e7740..e1da746c993 100644 --- a/src/models/event-timeline-set.ts +++ b/src/models/event-timeline-set.ts @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module models/event-timeline-set - */ - import { EventTimeline, IAddEventOptions } from "./event-timeline"; import { MatrixEvent } from "./event"; import { logger } from '../logger'; @@ -31,16 +27,20 @@ import { Thread, ThreadFilterType } from "./thread"; const DEBUG = true; +/* istanbul ignore next */ let debuglog: (...args: any[]) => void; if (DEBUG) { // using bind means that we get to keep useful line numbers in the console debuglog = logger.log.bind(logger); } else { + /* istanbul ignore next */ debuglog = function(): void {}; } interface IOpts { + // Set to true to enable improved timeline support. timelineSupport?: boolean; + // The filter object, if any, for this timelineSet. filter?: Filter; pendingEvents?: boolean; } @@ -51,7 +51,9 @@ export enum DuplicateStrategy { } export interface IRoomTimelineData { + // the timeline the event was added to/removed from timeline: EventTimeline; + // true if the event was a real-time event added to the end of the live timeline liveEvent?: boolean; } @@ -75,6 +77,26 @@ export interface IAddLiveEventOptions type EmittedEvents = RoomEvent.Timeline | RoomEvent.TimelineReset; export type EventTimelineSetHandlerMap = { + /** + * Fires whenever the timeline in a room is updated. + * @param event - The matrix event which caused this event to fire. + * @param room - The room, if any, whose timeline was updated. + * @param toStartOfTimeline - True if this event was added to the start + * @param removed - True if this event has just been removed from the timeline + * (beginning; oldest) of the timeline e.g. due to pagination. + * + * @param data - more data about the event + * + * @example + * ``` + * matrixClient.on("Room.timeline", + * function(event, room, toStartOfTimeline, removed, data) { + * if (!toStartOfTimeline && data.liveEvent) { + * var messageToAppend = room.timeline.[room.timeline.length - 1]; + * } + * }); + * ``` + */ [RoomEvent.Timeline]: ( event: MatrixEvent, room: Room | undefined, @@ -82,6 +104,18 @@ export type EventTimelineSetHandlerMap = { removed: boolean, data: IRoomTimelineData, ) => void; + /** + * Fires whenever the live timeline in a room is reset. + * + * When we get a 'limited' sync (for example, after a network outage), we reset + * the live timeline to be empty before adding the recent events to the new + * timeline. This event is fired after the timeline is reset, and before the + * new events are added. + * + * @param room - The room whose live timeline was reset, if any + * @param timelineSet - timelineSet room whose live timeline was reset + * @param resetAllTimelines - True if all timelines were reset. + */ [RoomEvent.TimelineReset]: ( room: Room | undefined, eventTimelineSet: EventTimelineSet, @@ -119,20 +153,13 @@ export class EventTimelineSet extends TypedEventEmitterIn order that we can find events from their ids later, we also maintain a * map from event_id to timeline and index. * - * @constructor - * @param {Room=} room - * Room for this timelineSet. May be null for non-room cases, such as the + * @param room - Room for this timelineSet. May be null for non-room cases, such as the * notification timeline. - * @param {Object} opts Options inherited from Room. - * - * @param {boolean} [opts.timelineSupport = false] - * Set to true to enable improved timeline support. - * @param {Object} [opts.filter = null] - * The filter object, if any, for this timelineSet. - * @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet, + * @param opts - Options inherited from Room. + * @param client - the Matrix client which owns this EventTimelineSet, * can be omitted if room is specified. - * @param {Thread=} thread the thread to which this timeline set relates. - * @param {boolean} isThreadTimeline Whether this timeline set relates to a thread list timeline + * @param thread - the thread to which this timeline set relates. + * @param isThreadTimeline - Whether this timeline set relates to a thread list timeline * (e.g., All threads or My threads) */ public constructor( @@ -159,7 +186,7 @@ export class EventTimelineSet extends TypedEventEmitteropts.pendingEventOrdering was not 'detached' + * @throws If `opts.pendingEventOrdering` was not 'detached' */ public getPendingEvents(): MatrixEvent[] { if (!this.room || !this.displayPendingEvents) { @@ -201,7 +228,7 @@ export class EventTimelineSet extends TypedEventEmitterThis is used when /sync returns a 'limited' timeline. * - * @param {string=} backPaginationToken token for back-paginating the new timeline - * @param {string=} forwardPaginationToken token for forward-paginating the old live timeline, + * @param backPaginationToken - token for back-paginating the new timeline + * @param forwardPaginationToken - token for forward-paginating the old live timeline, * if absent or null, all timelines are reset. * - * @fires module:client~MatrixClient#event:"Room.timelineReset" + * @remarks + * Fires {@link RoomEvent.TimelineReset} */ public resetLiveTimeline(backPaginationToken?: string, forwardPaginationToken?: string): void { // Each EventTimeline has RoomState objects tracking the state at the start @@ -293,8 +321,8 @@ export class EventTimelineSet extends TypedEventEmitterWill fire "Room.timeline" for each event added. * - * @param {MatrixEvent[]} events A list of events to add. + * @param events - A list of events to add. * - * @param {boolean} toStartOfTimeline True to add these events to the start + * @param toStartOfTimeline - True to add these events to the start * (oldest) instead of the end (newest) of the timeline. If true, the oldest * event will be the last element of 'events'. * - * @param {module:models/event-timeline~EventTimeline} timeline timeline to + * @param timeline - timeline to * add events to. * - * @param {string=} paginationToken token for the next batch of events + * @param paginationToken - token for the next batch of events * - * @fires module:client~MatrixClient#event:"Room.timeline" + * @remarks + * Fires {@link RoomEvent.Timeline} * */ public addEventsToTimeline( @@ -557,8 +586,8 @@ export class EventTimelineSet extends TypedEventEmitterOnce a timeline joins up with its neighbour, they are linked together into a * doubly-linked list. * - * @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of - * @constructor + * @param eventTimelineSet - the set of timelines this is part of */ public constructor(private readonly eventTimelineSet: EventTimelineSet) { this.roomId = eventTimelineSet.room?.roomId ?? null; @@ -148,9 +143,9 @@ export class EventTimeline { * *

This can only be called before any events are added. * - * @param {MatrixEvent[]} stateEvents list of state events to initialise the + * @param stateEvents - list of state events to initialise the * state with. - * @throws {Error} if an attempt is made to call this after addEvent is called. + * @throws Error if an attempt is made to call this after addEvent is called. */ public initialiseState(stateEvents: MatrixEvent[], { timelineWasEmpty }: IInitialiseStateOptions = {}): void { if (this.events.length > 0) { @@ -167,11 +162,11 @@ export class EventTimeline { * The end state of this timeline gets replaced with an independent copy of the current RoomState, * and will need a new pagination token if it ever needs to paginate forwards. - * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * @param direction - EventTimeline.BACKWARDS to get the state at the * start of the timeline; EventTimeline.FORWARDS to get the state at the end * of the timeline. * - * @return {EventTimeline} the new timeline + * @returns the new timeline */ public forkLive(direction: Direction): EventTimeline { const forkState = this.getState(direction); @@ -191,11 +186,11 @@ export class EventTimeline { /** * Creates an independent timeline, inheriting the directional state from this timeline. * - * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * @param direction - EventTimeline.BACKWARDS to get the state at the * start of the timeline; EventTimeline.FORWARDS to get the state at the end * of the timeline. * - * @return {EventTimeline} the new timeline + * @returns the new timeline */ public fork(direction: Direction): EventTimeline { const forkState = this.getState(direction); @@ -207,7 +202,7 @@ export class EventTimeline { /** * Get the ID of the room for this timeline - * @return {string} room ID + * @returns room ID */ public getRoomId(): string | null { return this.roomId; @@ -215,7 +210,7 @@ export class EventTimeline { /** * Get the filter for this timeline's timelineSet (if any) - * @return {Filter} filter + * @returns filter */ public getFilter(): Filter | undefined { return this.eventTimelineSet.getFilter(); @@ -223,7 +218,7 @@ export class EventTimeline { /** * Get the timelineSet for this timeline - * @return {EventTimelineSet} timelineSet + * @returns timelineSet */ public getTimelineSet(): EventTimelineSet { return this.eventTimelineSet; @@ -237,8 +232,6 @@ export class EventTimeline { * relative to the base index (although note that a given event's index may * well be less than the base index, thus giving that event a negative relative * index). - * - * @return {number} */ public getBaseIndex(): number { return this.baseIndex; @@ -247,7 +240,7 @@ export class EventTimeline { /** * Get the list of events in this context * - * @return {MatrixEvent[]} An array of MatrixEvents + * @returns An array of MatrixEvents */ public getEvents(): MatrixEvent[] { return this.events; @@ -256,11 +249,11 @@ export class EventTimeline { /** * Get the room state at the start/end of the timeline * - * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * @param direction - EventTimeline.BACKWARDS to get the state at the * start of the timeline; EventTimeline.FORWARDS to get the state at the end * of the timeline. * - * @return {RoomState} state at the start/end of the timeline + * @returns state at the start/end of the timeline */ public getState(direction: Direction): RoomState | undefined { if (direction == EventTimeline.BACKWARDS) { @@ -275,11 +268,11 @@ export class EventTimeline { /** * Get a pagination token * - * @param {string} direction EventTimeline.BACKWARDS to get the pagination + * @param direction - EventTimeline.BACKWARDS to get the pagination * token for going backwards in time; EventTimeline.FORWARDS to get the * pagination token for going forwards in time. * - * @return {?string} pagination token + * @returns pagination token */ public getPaginationToken(direction: Direction): string | null { if (this.roomId) { @@ -294,9 +287,9 @@ export class EventTimeline { /** * Set a pagination token * - * @param {?string} token pagination token + * @param token - pagination token * - * @param {string} direction EventTimeline.BACKWARDS to set the pagination + * @param direction - EventTimeline.BACKWARDS to set the pagination * token for going backwards in time; EventTimeline.FORWARDS to set the * pagination token for going forwards in time. */ @@ -313,10 +306,10 @@ export class EventTimeline { /** * Get the next timeline in the series * - * @param {string} direction EventTimeline.BACKWARDS to get the previous + * @param direction - EventTimeline.BACKWARDS to get the previous * timeline; EventTimeline.FORWARDS to get the next timeline. * - * @return {?EventTimeline} previous or following timeline, if they have been + * @returns previous or following timeline, if they have been * joined up. */ public getNeighbouringTimeline(direction: Direction): EventTimeline | null { @@ -332,12 +325,12 @@ export class EventTimeline { /** * Set the next timeline in the series * - * @param {EventTimeline} neighbour previous/following timeline + * @param neighbour - previous/following timeline * - * @param {string} direction EventTimeline.BACKWARDS to set the previous + * @param direction - EventTimeline.BACKWARDS to set the previous * timeline; EventTimeline.FORWARDS to set the next timeline. * - * @throws {Error} if an attempt is made to set the neighbouring timeline when + * @throws Error if an attempt is made to set the neighbouring timeline when * it is already set. */ public setNeighbouringTimeline(neighbour: EventTimeline, direction: Direction): void { @@ -361,8 +354,8 @@ export class EventTimeline { /** * Add a new event to the timeline, and update the state * - * @param {MatrixEvent} event new event - * @param {IAddEventOptions} options addEvent options + * @param event - new event + * @param options - addEvent options */ public addEvent( event: MatrixEvent, @@ -447,8 +440,8 @@ export class EventTimeline { /** * Remove an event from the timeline * - * @param {string} eventId ID of event to be removed - * @return {?MatrixEvent} removed event, or null if not found + * @param eventId - ID of event to be removed + * @returns removed event, or null if not found */ public removeEvent(eventId: string): MatrixEvent | null { for (let i = this.events.length - 1; i >= 0; i--) { @@ -467,7 +460,7 @@ export class EventTimeline { /** * Return a string to identify this timeline, for debugging * - * @return {string} name for this timeline + * @returns name for this timeline */ public toString(): string { return this.name; diff --git a/src/models/event.ts b/src/models/event.ts index 2864aa6b520..2b082f9b6dc 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -17,7 +17,6 @@ limitations under the License. /** * This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for * the public classes. - * @module models/event */ import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk"; @@ -80,15 +79,15 @@ export interface IEvent { redacts?: string; /** - * @deprecated + * @deprecated in favour of `sender` */ user_id?: string; /** - * @deprecated + * @deprecated in favour of `unsigned.prev_content` */ prev_content?: IContent; /** - * @deprecated + * @deprecated in favour of `origin_server_ts` */ age?: number; } @@ -150,8 +149,11 @@ interface IKeyRequestRecipient { } export interface IDecryptOptions { + // Emits "event.decrypted" if set to true emit?: boolean; + // True if this is a retry (enables more logging) isRetry?: boolean; + // whether the message should be re-decrypted if it was previously successfully decrypted with an untrusted key forceRedecryptIfUntrusted?: boolean; } @@ -192,6 +194,12 @@ export enum MatrixEventEvent { export type MatrixEventEmittedEvents = MatrixEventEvent | ThreadEvent.Update; export type MatrixEventHandlerMap = { + /** + * Fires when an event is decrypted + * + * @param event - The matrix event which has been decrypted + * @param err - The error that occurred during decryption, or `undefined` if no error occurred. + */ [MatrixEventEvent.Decrypted]: (event: MatrixEvent, err?: Error) => void; [MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void; [MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void; @@ -271,12 +279,43 @@ export class MatrixEvent extends TypedEventEmitterThis property is experimental and may change. + * @privateRemarks + * Should be read-only + */ + public forwardLooking = true; /* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event, * `Crypto` will set this the `VerificationRequest` for the event @@ -288,26 +327,11 @@ export class MatrixEvent extends TypedEventEmitterDo not access + * @param event - The raw (possibly encrypted) event. Do not access * this property directly unless you absolutely have to. Prefer the getter * methods defined on this class. Using the getter methods shields your app * from changes to event JSON between Matrix versions. - * - * @prop {RoomMember} sender The room member who sent this event, or null e.g. - * this is a presence event. This is only guaranteed to be set for events that - * appear in a timeline, ie. do not guarantee that it will be set on state - * events. - * @prop {RoomMember} target The room member who is the target of this event, e.g. - * the invitee, the person being banned, etc. - * @prop {EventStatus} status The sending status of the event. - * @prop {Error} error most recent error associated with sending the event, if any - * @prop {boolean} forwardLooking True if this event is 'forward looking', meaning - * that getDirectionalContent() will return event.content and not event.prev_content. - * Default: true. This property is experimental and may change. */ public constructor(public event: Partial = {}) { super(); @@ -360,7 +384,7 @@ export class MatrixEvent extends TypedEventEmitter$143350589368169JsLZx:localhost + * @returns The event ID, e.g. $143350589368169JsLZx:localhost * */ public getId(): string | undefined { @@ -398,7 +422,7 @@ export class MatrixEvent extends TypedEventEmitter@alice:matrix.org + * @returns The user ID, e.g. `@alice:matrix.org` */ public getSender(): string | undefined { return this.event.sender || this.event.user_id; // v2 / v1 @@ -407,7 +431,7 @@ export class MatrixEvent extends TypedEventEmitterm.room.message + * @returns The event type, e.g. `m.room.message` */ public getType(): EventType | string { if (this.clearEvent) { @@ -420,16 +444,16 @@ export class MatrixEvent extends TypedEventEmitterundefined - * for m.presence events. - * @return {string?} The room ID, e.g. !cURbafjkfsMDVwdRDQ:matrix.org + * Get the room_id for this event. This will return `undefined` + * for `m.presence` events. + * @returns The room ID, e.g. !cURbafjkfsMDVwdRDQ:matrix.org * */ public getRoomId(): string | undefined { @@ -438,7 +462,7 @@ export class MatrixEvent extends TypedEventEmitter1433502692297 + * @returns The event timestamp, e.g. `1433502692297` */ public getTs(): number { return this.event.origin_server_ts!; @@ -446,7 +470,7 @@ export class MatrixEvent extends TypedEventEmitternew Date(1433502692297) + * @returns The event date, e.g. `new Date(1433502692297)` */ public getDate(): Date | null { return this.event.origin_server_ts ? new Date(this.event.origin_server_ts) : null; @@ -457,7 +481,11 @@ export class MatrixEvent extends TypedEventEmitter(): T { if (this._localRedactionEvent) { @@ -493,7 +521,7 @@ export class MatrixEvent extends TypedEventEmitter(): T { if (this._localRedactionEvent) { @@ -509,7 +537,7 @@ export class MatrixEvent extends TypedEventEmitterThis method is experimental and may change. - * @return {Object} event.content if this event is forward-looking, else + * @returns event.content if this event is forward-looking, else * event.prev_content. */ public getDirectionalContent(): IContent { @@ -585,7 +613,7 @@ export class MatrixEvent extends TypedEventEmitterundefined * for message events. - * @return {string} The event's state_key. + * @returns The event's `state_key`. */ public getStateKey(): string | undefined { return this.event.state_key; @@ -612,7 +640,7 @@ export class MatrixEvent extends TypedEventEmitter"m.room.encrypted" * - * @param {object} cryptoContent raw 'content' for the encrypted event. + * @param cryptoContent - raw 'content' for the encrypted event. * - * @param {string} senderCurve25519Key curve25519 key to record for the + * @param senderCurve25519Key - curve25519 key to record for the * sender of this event. - * See {@link module:models/event.MatrixEvent#getSenderKey}. + * See {@link MatrixEvent#getSenderKey}. * - * @param {string} claimedEd25519Key claimed ed25519 key to record for the + * @param claimedEd25519Key - claimed ed25519 key to record for the * sender if this event. - * See {@link module:models/event.MatrixEvent#getClaimedEd25519Key} + * See {@link MatrixEvent#getClaimedEd25519Key} */ public makeEncrypted( cryptoType: string, @@ -657,7 +685,7 @@ export class MatrixEvent extends TypedEventEmitter { @@ -741,10 +764,10 @@ export class MatrixEvent extends TypedEventEmitter { const wireContent = this.getWireContent(); @@ -759,9 +782,9 @@ export class MatrixEvent extends TypedEventEmitter} */ public getKeysClaimed(): Partial> { if (!this.claimedEd25519Key) return {}; @@ -982,8 +1001,6 @@ export class MatrixEvent extends TypedEventEmitter

When an event is first transmitted, a temporary copy of the event is + * inserted into the timeline, with a temporary event id, and a status of + * 'SENDING'. + * + *

Once the echo comes back from the server, the content of the event + * (MatrixEvent.event) is replaced by the complete event from the homeserver, + * thus updating its event id, as well as server-generated fields such as the + * timestamp. Its status is set to null. + * + *

Once the /send request completes, if the remote echo has not already + * arrived, the event is updated with a new event id and the status is set to + * 'SENT'. The server-generated fields are of course not updated yet. + * + *

If the /send fails, In this case, the event's status is set to + * 'NOT_SENT'. If it is later resent, the process starts again, setting the + * status to 'SENDING'. Alternatively, the message may be cancelled, which + * removes the event from the room, and sets the status to 'CANCELLED'. + * + *

This event is raised to reflect each of the transitions above. + * + * @param event - The matrix event which has been updated + * + * @param room - The room containing the redacted event + * + * @param oldEventId - The previous event id (the temporary event id, + * except when updating a successfully-sent event when its echo arrives) + * + * @param oldStatus - The previous event status. + */ [RoomEvent.LocalEchoUpdated]: ( event: MatrixEvent, room: Room, @@ -227,7 +337,7 @@ export class Room extends ReadReceipt { public normalizedName: string; /** * Dict of room tags; the keys are the tag name and the values - * are any metadata associated with the tag - e.g. { "fav" : { order: 1 } } + * are any metadata associated with the tag - e.g. `{ "fav" : { order: 1 } }` */ public tags: Record> = {}; // $tagName: { $metadata: $value } /** @@ -301,21 +411,10 @@ export class Room extends ReadReceipt { *

In order that we can find events from their ids later, we also maintain a * map from event_id to timeline and index. * - * @constructor - * @alias module:models/room - * @param {string} roomId Required. The ID of this room. - * @param {MatrixClient} client Required. The client, used to lazy load members. - * @param {string} myUserId Required. The ID of the syncing user. - * @param {Object=} opts Configuration options - * - * @param {String=} opts.pendingEventOrdering Controls where pending messages - * appear in a room's timeline. If "chronological", messages will appear - * in the timeline when the call to sendEvent was made. If - * "detached", pending messages will appear in a separate list, - * accessible via {@link module:models/room#getPendingEvents}. Default: - * "chronological". - * @param {boolean} [opts.timelineSupport = false] Set to true to enable improved - * timeline support. + * @param roomId - Required. The ID of this room. + * @param client - Required. The client, used to lazy load members. + * @param myUserId - Required. The ID of the syncing user. + * @param opts - Configuration options */ public constructor( public readonly roomId: string, @@ -402,7 +501,7 @@ export class Room extends ReadReceipt { * - Last event of every room (to generate likely message preview) * - All events up to the read receipt (to calculate an accurate notification count) * - * @returns {Promise} Signals when all events have been decrypted + * @returns Signals when all events have been decrypted */ public async decryptCriticalEvents(): Promise { if (!this.client.isCryptoEnabled()) return; @@ -425,7 +524,7 @@ export class Room extends ReadReceipt { /** * Bulk decrypt events in a room * - * @returns {Promise} Signals when all events have been decrypted + * @returns Signals when all events have been decrypted */ public async decryptAllEvents(): Promise { if (!this.client.isCryptoEnabled()) return; @@ -443,7 +542,7 @@ export class Room extends ReadReceipt { /** * Gets the creator of the room - * @returns {string} The creator of the room, or null if it could not be determined + * @returns The creator of the room, or null if it could not be determined */ public getCreator(): string | null { const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, ""); @@ -452,7 +551,7 @@ export class Room extends ReadReceipt { /** * Gets the version of the room - * @returns {string} The version of the room, or null if it could not be determined + * @returns The version of the room, or null if it could not be determined */ public getVersion(): string { const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, ""); @@ -468,7 +567,7 @@ export class Room extends ReadReceipt { /** * Determines whether this room needs to be upgraded to a new version - * @returns {string?} What version the room should be upgraded to, or null if + * @returns What version the room should be upgraded to, or null if * the room does not require upgrading at this time. * @deprecated Use #getRecommendedVersion() instead */ @@ -489,13 +588,13 @@ export class Room extends ReadReceipt { /** * Determines the recommended room version for the room. This returns an - * object with 3 properties: version as the new version the + * object with 3 properties: `version` as the new version the * room should be upgraded to (may be the same as the current version); - * needsUpgrade to indicate if the room actually can be - * upgraded (ie: does the current version not match?); and urgent + * `needsUpgrade` to indicate if the room actually can be + * upgraded (ie: does the current version not match?); and `urgent` * to indicate if the new version patches a vulnerability in a previous * version. - * @returns {Promise<{version: string, needsUpgrade: boolean, urgent: boolean}>} + * @returns * Resolves to the version the room should be upgraded to. */ public async getRecommendedVersion(): Promise { @@ -576,8 +675,8 @@ export class Room extends ReadReceipt { /** * Determines whether the given user is permitted to perform a room upgrade - * @param {String} userId The ID of the user to test against - * @returns {boolean} True if the given user is permitted to upgrade the room + * @param userId - The ID of the user to test against + * @returns True if the given user is permitted to upgrade the room */ public userMayUpgradeRoom(userId: string): boolean { return this.currentState.maySendStateEvent(EventType.RoomTombstone, userId); @@ -586,10 +685,10 @@ export class Room extends ReadReceipt { /** * Get the list of pending sent events for this room * - * @return {module:models/event.MatrixEvent[]} A list of the sent events + * @returns A list of the sent events * waiting for remote echo. * - * @throws If opts.pendingEventOrdering was not 'detached' + * @throws If `opts.pendingEventOrdering` was not 'detached' */ public getPendingEvents(): MatrixEvent[] { if (!this.pendingEventList) { @@ -604,8 +703,7 @@ export class Room extends ReadReceipt { /** * Removes a pending event for this room * - * @param {string} eventId - * @return {boolean} True if an element was removed. + * @returns True if an element was removed. */ public removePendingEvent(eventId: string): boolean { if (!this.pendingEventList) { @@ -630,8 +728,7 @@ export class Room extends ReadReceipt { * Check whether the pending event list contains a given event by ID. * If pending event ordering is not "detached" then this returns false. * - * @param {string} eventId The event ID to check for. - * @return {boolean} + * @param eventId - The event ID to check for. */ public hasPendingEvent(eventId: string): boolean { return this.pendingEventList?.some(event => event.getId() === eventId) ?? false; @@ -640,8 +737,7 @@ export class Room extends ReadReceipt { /** * Get a specific event from the pending event list, if configured, null otherwise. * - * @param {string} eventId The event ID to check for. - * @return {MatrixEvent} + * @param eventId - The event ID to check for. */ public getPendingEvent(eventId: string): MatrixEvent | null { return this.pendingEventList?.find(event => event.getId() === eventId) ?? null; @@ -650,7 +746,7 @@ export class Room extends ReadReceipt { /** * Get the live unfiltered timeline for this room. * - * @return {module:models/event-timeline~EventTimeline} live timeline + * @returns live timeline */ public getLiveTimeline(): EventTimeline { return this.getUnfilteredTimelineSet().getLiveTimeline(); @@ -659,7 +755,7 @@ export class Room extends ReadReceipt { /** * Get the timestamp of the last message in the room * - * @return {number} the timestamp of the last message in the room + * @returns the timestamp of the last message in the room */ public getLastActiveTimestamp(): number { const timeline = this.getLiveTimeline(); @@ -673,7 +769,7 @@ export class Room extends ReadReceipt { } /** - * @return {string} the membership type (join | leave | invite) for the logged in user + * @returns the membership type (join | leave | invite) for the logged in user */ public getMyMembership(): string { return this.selfMembership ?? "leave"; @@ -682,7 +778,7 @@ export class Room extends ReadReceipt { /** * If this room is a DM we're invited to, * try to find out who invited us - * @return {string} user id of the inviter + * @returns user id of the inviter */ public getDMInviter(): string | undefined { const me = this.getMember(this.myUserId); @@ -701,7 +797,7 @@ export class Room extends ReadReceipt { /** * Assuming this room is a DM room, tries to guess with which user. - * @return {string} user id of the other member (could be syncing user) + * @returns user id of the other member (could be syncing user) */ public guessDMUserId(): string { const me = this.getMember(this.myUserId); @@ -768,7 +864,7 @@ export class Room extends ReadReceipt { /** * Sets the membership this room was received as during sync - * @param {string} membership join | leave | invite + * @param membership - join | leave | invite */ public updateMyMembership(membership: string): void { const prevMembership = this.selfMembership; @@ -812,7 +908,7 @@ export class Room extends ReadReceipt { * Preloads the member list in case lazy loading * of memberships is in use. Can be called multiple times, * it will only preload once. - * @return {Promise} when preloading is done and + * @returns when preloading is done and * accessing the members on the room will take * all members in the room into account */ @@ -893,7 +989,7 @@ export class Room extends ReadReceipt { /** * Empty out the current live timeline and re-request it. This is used when - * historical messages are imported into the room via MSC2716 `/batch_send + * historical messages are imported into the room via MSC2716 `/batch_send` * because the client may already have that section of the timeline loaded. * We need to force the client to throw away their current timeline so that * when they back paginate over the area again with the historical messages @@ -998,8 +1094,8 @@ export class Room extends ReadReceipt { * *

This is used when /sync returns a 'limited' timeline. * - * @param {string=} backPaginationToken token for back-paginating the new timeline - * @param {string=} forwardPaginationToken token for forward-paginating the old live timeline, + * @param backPaginationToken - token for back-paginating the new timeline + * @param forwardPaginationToken - token for forward-paginating the old live timeline, * if absent or null, all timelines are reset, removing old ones (including the previous live * timeline which would otherwise be unable to paginate forwards without this token). * Removing just the old live timeline whilst preserving previous ones is not supported. @@ -1018,7 +1114,7 @@ export class Room extends ReadReceipt { /** * Fix up this.timeline, this.oldState and this.currentState * - * @private + * @internal */ private fixUpLegacyTimelineFields(): void { const previousOldState = this.oldState; @@ -1077,7 +1173,7 @@ export class Room extends ReadReceipt { * disabled, then we aren't tracking room devices at all, so we can't answer this, and an * error will be thrown. * - * @return {boolean} the result + * @returns the result */ public async hasUnverifiedDevices(): Promise { if (!this.client.isRoomEncrypted(this.roomId)) { @@ -1095,7 +1191,7 @@ export class Room extends ReadReceipt { /** * Return the timeline sets for this room. - * @return {EventTimelineSet[]} array of timeline sets for this room + * @returns array of timeline sets for this room */ public getTimelineSets(): EventTimelineSet[] { return this.timelineSets; @@ -1103,7 +1199,7 @@ export class Room extends ReadReceipt { /** * Helper to return the main unfiltered timeline set for this room - * @return {EventTimelineSet} room's unfiltered timeline set + * @returns room's unfiltered timeline set */ public getUnfilteredTimelineSet(): EventTimelineSet { return this.timelineSets[0]; @@ -1112,8 +1208,8 @@ export class Room extends ReadReceipt { /** * Get the timeline which contains the given event from the unfiltered set, if any * - * @param {string} eventId event ID to look for - * @return {?module:models/event-timeline~EventTimeline} timeline containing + * @param eventId - event ID to look for + * @returns timeline containing * the given event, or null if unknown */ public getTimelineForEvent(eventId: string): EventTimeline | null { @@ -1129,7 +1225,7 @@ export class Room extends ReadReceipt { /** * Add a new timeline to this room's unfiltered timeline set * - * @return {module:models/event-timeline~EventTimeline} newly-created timeline + * @returns newly-created timeline */ public addTimeline(): EventTimeline { return this.getUnfilteredTimelineSet().addTimeline(); @@ -1138,7 +1234,7 @@ export class Room extends ReadReceipt { /** * Whether the timeline needs to be refreshed in order to pull in new * historical messages that were imported. - * @param {Boolean} value The value to set + * @param value - The value to set */ public setTimelineNeedsRefresh(value: boolean): void { this.timelineNeedsRefresh = value; @@ -1147,7 +1243,7 @@ export class Room extends ReadReceipt { /** * Whether the timeline needs to be refreshed in order to pull in new * historical messages that were imported. - * @return {Boolean} . + * @returns . */ public getTimelineNeedsRefresh(): boolean { return this.timelineNeedsRefresh; @@ -1156,8 +1252,8 @@ export class Room extends ReadReceipt { /** * Get an event which is stored in our unfiltered timeline set, or in a thread * - * @param {string} eventId event ID to look for - * @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown + * @param eventId - event ID to look for + * @returns the given event, or undefined if unknown */ public findEventById(eventId: string): MatrixEvent | undefined { let event = this.getUnfilteredTimelineSet().findEventById(eventId); @@ -1178,8 +1274,8 @@ export class Room extends ReadReceipt { /** * Get one of the notification counts for this room - * @param {String} type The type of notification count to get. default: 'total' - * @return {Number} The notification count, or undefined if there is no count + * @param type - The type of notification count to get. default: 'total' + * @returns The notification count, or undefined if there is no count * for this type. */ public getUnreadNotificationCount(type = NotificationCountType.Total): number { @@ -1205,8 +1301,8 @@ export class Room extends ReadReceipt { /** * Get one of the notification counts for this room - * @param {String} type The type of notification count to get. default: 'total' - * @return {Number} The notification count, or undefined if there is no count + * @param type - The type of notification count to get. default: 'total' + * @returns The notification count, or undefined if there is no count * for this type. */ public getRoomUnreadNotificationCount(type = NotificationCountType.Total): number { @@ -1216,8 +1312,8 @@ export class Room extends ReadReceipt { /** * @experimental * Get one of the notification counts for a thread - * @param threadId the root event ID - * @param type The type of notification count to get. default: 'total' + * @param threadId - the root event ID + * @param type - The type of notification count to get. default: 'total' * @returns The notification count, or undefined if there is no count * for this type. */ @@ -1228,7 +1324,7 @@ export class Room extends ReadReceipt { /** * @experimental * Checks if the current room has unread thread notifications - * @returns {boolean} + * @returns */ public hasThreadUnreadNotification(): boolean { for (const notification of this.threadNotifications.values()) { @@ -1242,9 +1338,9 @@ export class Room extends ReadReceipt { /** * @experimental * Swet one of the notification count for a thread - * @param threadId the root event ID - * @param type The type of notification count to get. default: 'total' - * @returns {void} + * @param threadId - the root event ID + * @param type - The type of notification count to get. default: 'total' + * @returns */ public setThreadUnreadNotificationCount(threadId: string, type: NotificationCountType, count: number): void { const notification: NotificationCount = { @@ -1299,8 +1395,8 @@ export class Room extends ReadReceipt { /** * Set one of the notification counts for this room - * @param {String} type The type of notification count to set. - * @param {Number} count The new count + * @param type - The type of notification count to set. + * @param count - The new count */ public setUnreadNotificationCount(type: NotificationCountType, count: number): void { this.notificationCounts[type] = count; @@ -1329,7 +1425,7 @@ export class Room extends ReadReceipt { /** * Whether to send encrypted messages to devices within this room. - * @param {Boolean} value true to blacklist unverified devices, null + * @param value - true to blacklist unverified devices, null * to use the global value for this room. */ public setBlacklistUnverifiedDevices(value: boolean): void { @@ -1338,7 +1434,7 @@ export class Room extends ReadReceipt { /** * Whether to send encrypted messages to devices within this room. - * @return {Boolean} true if blacklisting unverified devices, null + * @returns true if blacklisting unverified devices, null * if the global value should be used for this room. */ public getBlacklistUnverifiedDevices(): boolean | null { @@ -1348,15 +1444,15 @@ export class Room extends ReadReceipt { /** * Get the avatar URL for a room if one was set. - * @param {String} baseUrl The homeserver base URL. See - * {@link module:client~MatrixClient#getHomeserverUrl}. - * @param {Number} width The desired width of the thumbnail. - * @param {Number} height The desired height of the thumbnail. - * @param {string} resizeMethod The thumbnail resize method to use, either + * @param baseUrl - The homeserver base URL. See + * {@link MatrixClient#getHomeserverUrl}. + * @param width - The desired width of the thumbnail. + * @param height - The desired height of the thumbnail. + * @param resizeMethod - The thumbnail resize method to use, either * "crop" or "scale". - * @param {boolean} allowDefault True to allow an identicon for this room if an + * @param allowDefault - True to allow an identicon for this room if an * avatar URL wasn't explicitly set. Default: true. (Deprecated) - * @return {?string} the avatar URL or null. + * @returns the avatar URL or null. */ public getAvatarUrl( baseUrl: string, @@ -1380,7 +1476,7 @@ export class Room extends ReadReceipt { /** * Get the mxc avatar url for the room, if one was set. - * @return {string} the mxc avatar url or falsy + * @returns the mxc avatar url or falsy */ public getMxcAvatarUrl(): string | null { return this.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url || null; @@ -1390,7 +1486,7 @@ export class Room extends ReadReceipt { * Get this room's canonical alias * The alias returned by this function may not necessarily * still point to this room. - * @return {?string} The room's canonical alias, or null if there is none + * @returns The room's canonical alias, or null if there is none */ public getCanonicalAlias(): string | null { const canonicalAlias = this.currentState.getStateEvents(EventType.RoomCanonicalAlias, ""); @@ -1402,7 +1498,7 @@ export class Room extends ReadReceipt { /** * Get this room's alternative aliases - * @return {array} The room's alternative aliases, or an empty array + * @returns The room's alternative aliases, or an empty array */ public getAltAliases(): string[] { const canonicalAlias = this.currentState.getStateEvents(EventType.RoomCanonicalAlias, ""); @@ -1417,19 +1513,19 @@ export class Room extends ReadReceipt { * *

Will fire "Room.timeline" for each event added. * - * @param {MatrixEvent[]} events A list of events to add. + * @param events - A list of events to add. * - * @param {boolean} toStartOfTimeline True to add these events to the start + * @param toStartOfTimeline - True to add these events to the start * (oldest) instead of the end (newest) of the timeline. If true, the oldest * event will be the last element of 'events'. * - * @param {module:models/event-timeline~EventTimeline} timeline timeline to + * @param timeline - timeline to * add events to. * - * @param {string=} paginationToken token for the next batch of events - * - * @fires module:client~MatrixClient#event:"Room.timeline" + * @param paginationToken - token for the next batch of events * + * @remarks + * Fires {@link RoomEvent.Timeline} */ public addEventsToTimeline( events: MatrixEvent[], @@ -1456,8 +1552,8 @@ export class Room extends ReadReceipt { /** * Get a member from the current room state. - * @param {string} userId The user ID of the member. - * @return {RoomMember} The member or null. + * @param userId - The user ID of the member. + * @returns The member or `null`. */ public getMember(userId: string): RoomMember | null { return this.currentState.getMember(userId); @@ -1466,7 +1562,7 @@ export class Room extends ReadReceipt { /** * Get all currently loaded members from the current * room state. - * @returns {RoomMember[]} Room members + * @returns Room members */ public getMembers(): RoomMember[] { return this.currentState.getMembers(); @@ -1474,7 +1570,7 @@ export class Room extends ReadReceipt { /** * Get a list of members whose membership state is "join". - * @return {RoomMember[]} A list of currently joined members. + * @returns A list of currently joined members. */ public getJoinedMembers(): RoomMember[] { return this.getMembersWithMembership("join"); @@ -1485,7 +1581,7 @@ export class Room extends ReadReceipt { * This method caches the result. * This is a wrapper around the method of the same name in roomState, returning * its result for the room's current state. - * @return {number} The number of members in this room whose membership is 'join' + * @returns The number of members in this room whose membership is 'join' */ public getJoinedMemberCount(): number { return this.currentState.getJoinedMemberCount(); @@ -1493,7 +1589,7 @@ export class Room extends ReadReceipt { /** * Returns the number of invited members in this room - * @return {number} The number of members in this room whose membership is 'invite' + * @returns The number of members in this room whose membership is 'invite' */ public getInvitedMemberCount(): number { return this.currentState.getInvitedMemberCount(); @@ -1501,7 +1597,7 @@ export class Room extends ReadReceipt { /** * Returns the number of invited + joined members in this room - * @return {number} The number of members in this room whose membership is 'invite' or 'join' + * @returns The number of members in this room whose membership is 'invite' or 'join' */ public getInvitedAndJoinedMemberCount(): number { return this.getInvitedMemberCount() + this.getJoinedMemberCount(); @@ -1509,8 +1605,8 @@ export class Room extends ReadReceipt { /** * Get a list of members with given membership state. - * @param {string} membership The membership state. - * @return {RoomMember[]} A list of members with the given membership state. + * @param membership - The membership state. + * @returns A list of members with the given membership state. */ public getMembersWithMembership(membership: string): RoomMember[] { return this.currentState.getMembers().filter(function(m) { @@ -1520,7 +1616,7 @@ export class Room extends ReadReceipt { /** * Get a list of members we should be encrypting for in this room - * @return {Promise} A list of members who + * @returns A list of members who * we should encrypt messages for in this room. */ public async getEncryptionTargetMembers(): Promise { @@ -1534,7 +1630,7 @@ export class Room extends ReadReceipt { /** * Determine whether we should encrypt messages for invited users in this room - * @return {boolean} if we should encrypt messages for invited users + * @returns if we should encrypt messages for invited users */ public shouldEncryptForInvitedMembers(): boolean { const ev = this.currentState.getStateEvents(EventType.RoomHistoryVisibility, ""); @@ -1544,9 +1640,9 @@ export class Room extends ReadReceipt { /** * Get the default room name (i.e. what a given user would see if the * room had no m.room.name) - * @param {string} userId The userId from whose perspective we want + * @param userId - The userId from whose perspective we want * to calculate the default name - * @return {string} The default room name + * @returns The default room name */ public getDefaultRoomName(userId: string): string { return this.calculateRoomName(userId, true); @@ -1554,9 +1650,9 @@ export class Room extends ReadReceipt { /** * Check if the given user_id has the given membership state. - * @param {string} userId The user ID to check. - * @param {string} membership The membership e.g. 'join' - * @return {boolean} True if this user_id has the given membership state. + * @param userId - The user ID to check. + * @param membership - The membership e.g. `'join'` + * @returns True if this user_id has the given membership state. */ public hasMembershipState(userId: string, membership: string): boolean { const member = this.getMember(userId); @@ -1568,9 +1664,9 @@ export class Room extends ReadReceipt { /** * Add a timelineSet for this room with the given filter - * @param {Filter} filter The filter to be applied to this timelineSet - * @param {Object=} opts Configuration options - * @return {EventTimelineSet} The timelineSet + * @param filter - The filter to be applied to this timelineSet + * @param opts - Configuration options + * @returns The timelineSet */ public getOrCreateFilteredTimelineSet( filter: Filter, @@ -1714,8 +1810,6 @@ export class Room extends ReadReceipt { /** * Takes the given thread root events and creates threads for them. - * @param events - * @param toStartOfTimeline */ public processThreadRoots(events: MatrixEvent[], toStartOfTimeline: boolean): void { for (const rootEvent of events) { @@ -1809,8 +1903,7 @@ export class Room extends ReadReceipt { /** * Fetch a single page of threadlist messages for the specific thread filter - * @param filter - * @private + * @internal */ private async fetchRoomThreadList(filter?: ThreadFilterType): Promise { const timelineSet = filter === ThreadFilterType.My @@ -1864,7 +1957,7 @@ export class Room extends ReadReceipt { /** * Forget the timelineSet for this room with the given filter * - * @param {Filter} filter the filter whose timelineSet is to be forgotten + * @param filter - the filter whose timelineSet is to be forgotten */ public removeFilteredTimelineSet(filter: Filter): void { const timelineSet = this.filteredTimelineSets[filter.filterId!]; @@ -2142,10 +2235,12 @@ export class Room extends ReadReceipt { * Add an event to the end of this room's live timelines. Will fire * "Room.timeline". * - * @param {MatrixEvent} event Event to be added - * @param {IAddLiveEventOptions} addLiveEventOptions addLiveEvent options - * @fires module:client~MatrixClient#event:"Room.timeline" - * @private + * @param event - Event to be added + * @param addLiveEventOptions - addLiveEvent options + * @internal + * + * @remarks + * Fires {@link RoomEvent.Timeline} */ private addLiveEvent(event: MatrixEvent, addLiveEventOptions: IAddLiveEventOptions): void { const { duplicateStrategy, timelineWasEmpty, fromCache } = addLiveEventOptions; @@ -2185,14 +2280,15 @@ export class Room extends ReadReceipt { * *

This is an internal method, intended for use by MatrixClient. * - * @param {module:models/event.MatrixEvent} event The event to add. + * @param event - The event to add. * - * @param {string} txnId Transaction id for this outgoing event - * - * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" + * @param txnId - Transaction id for this outgoing event * * @throws if the event doesn't have status SENDING, or we aren't given a * unique transaction id. + * + * @remarks + * Fires {@link RoomEvent.LocalEchoUpdated} */ public addPendingEvent(event: MatrixEvent, txnId: string): void { if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) { @@ -2264,7 +2360,7 @@ export class Room extends ReadReceipt { * all messages that are not yet encrypted will be discarded * * This is because the flow of EVENT_STATUS transition is - * queued => sending => encrypting => sending => sent + * `queued => sending => encrypting => sending => sent` * * Steps 3 and 4 are skipped for unencrypted room. * It is better to discard an unencrypted message rather than persisting @@ -2296,7 +2392,7 @@ export class Room extends ReadReceipt { * which are just kept detached for their local echo. * * Also note that live events are aggregated in the live EventTimelineSet. - * @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated. + * @param event - the relation event that needs to be aggregated. */ private aggregateNonLiveRelation(event: MatrixEvent): void { this.relations.aggregateChildEvent(event); @@ -2312,13 +2408,15 @@ export class Room extends ReadReceipt { *

We move the event to the live timeline if it isn't there already, and * update it. * - * @param {module:models/event.MatrixEvent} remoteEvent The event received from + * @param remoteEvent - The event received from * /sync - * @param {module:models/event.MatrixEvent} localEvent The local echo, which + * @param localEvent - The local echo, which * should be either in the pendingEventList or the timeline. * - * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" - * @private + * @internal + * + * @remarks + * Fires {@link RoomEvent.LocalEchoUpdated} */ public handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void { const oldEventId = localEvent.getId()!; @@ -2359,11 +2457,12 @@ export class Room extends ReadReceipt { * *

This is an internal method. * - * @param {MatrixEvent} event local echo event - * @param {EventStatus} newStatus status to assign - * @param {string} newEventId new event id to assign. Ignored unless - * newStatus == EventStatus.SENT. - * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" + * @param event - local echo event + * @param newStatus - status to assign + * @param newEventId - new event id to assign. Ignored unless newStatus == EventStatus.SENT. + * + * @remarks + * Fires {@link RoomEvent.LocalEchoUpdated} */ public updatePendingEvent(event: MatrixEvent, newStatus: EventStatus, newEventId?: string): void { logger.log( @@ -2470,9 +2569,9 @@ export class Room extends ReadReceipt { * events and typing notifications. These events are treated as "live" so * they will go to the end of the timeline. * - * @param {MatrixEvent[]} events A list of events to add. - * @param {IAddLiveEventOptions} addLiveEventOptions addLiveEvent options - * @throws If duplicateStrategy is not falsey, 'replace' or 'ignore'. + * @param events - A list of events to add. + * @param addLiveEventOptions - addLiveEvent options + * @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'. */ public addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): void; /** @@ -2615,8 +2714,8 @@ export class Room extends ReadReceipt { /** * Add a receipt event to the room. - * @param {MatrixEvent} event The m.receipt event. - * @param {Boolean} synthetic True if this event is implicit. + * @param event - The m.receipt event. + * @param synthetic - True if this event is implicit. */ public addReceipt(event: MatrixEvent, synthetic = false): void { const content = event.getContent(); @@ -2646,7 +2745,7 @@ export class Room extends ReadReceipt { /** * Adds/handles ephemeral events such as typing notifications and read receipts. - * @param {MatrixEvent[]} events A list of events to process + * @param events - A list of events to process */ public addEphemeralEvents(events: MatrixEvent[]): void { for (const event of events) { @@ -2660,7 +2759,7 @@ export class Room extends ReadReceipt { /** * Removes events from this room. - * @param {String[]} eventIds A list of eventIds to remove. + * @param eventIds - A list of eventIds to remove. */ public removeEvents(eventIds: string[]): void { for (const eventId of eventIds) { @@ -2671,9 +2770,9 @@ export class Room extends ReadReceipt { /** * Removes a single event from this room. * - * @param {String} eventId The id of the event to remove + * @param eventId - The id of the event to remove * - * @return {boolean} true if the event was removed from any of the room's timeline sets + * @returns true if the event was removed from any of the room's timeline sets */ public removeEvent(eventId: string): boolean { let removedAny = false; @@ -2693,7 +2792,9 @@ export class Room extends ReadReceipt { * Recalculate various aspects of the room, including the room name and * room summary. Call this any time the room's current state is modified. * May fire "Room.name" if the room name is updated. - * @fires module:client~MatrixClient#event:"Room.name" + * + * @remarks + * Fires {@link RoomEvent.Name} */ public recalculate(): void { // set fake stripped state events if this is an invite room so logic remains @@ -2736,7 +2837,7 @@ export class Room extends ReadReceipt { /** * Update the room-tag event for the room. The previous one is overwritten. - * @param {MatrixEvent} event the m.tag event + * @param event - the m.tag event */ public addTags(event: MatrixEvent): void { // event content looks like: @@ -2757,7 +2858,7 @@ export class Room extends ReadReceipt { /** * Update the account_data events for this room, overwriting events of the same type. - * @param {Array} events an array of account_data events to add + * @param events - an array of account_data events to add */ public addAccountData(events: MatrixEvent[]): void { for (const event of events) { @@ -2772,8 +2873,8 @@ export class Room extends ReadReceipt { /** * Access account_data event of given event type for this room - * @param {string} type the type of account_data event to be accessed - * @return {?MatrixEvent} the account_data event in question + * @param type - the type of account_data event to be accessed + * @returns the account_data event in question */ public getAccountData(type: EventType | string): MatrixEvent | undefined { return this.accountData[type]; @@ -2781,7 +2882,7 @@ export class Room extends ReadReceipt { /** * Returns whether the syncing user has permission to send a message in the room - * @return {boolean} true if the user should be permitted to send + * @returns true if the user should be permitted to send * message events into the room. */ public maySendMessage(): boolean { @@ -2792,8 +2893,8 @@ export class Room extends ReadReceipt { /** * Returns whether the given user has permissions to issue an invite for this room. - * @param {string} userId the ID of the Matrix user to check permissions for - * @returns {boolean} true if the user should be permitted to issue invites for this room. + * @param userId - the ID of the Matrix user to check permissions for + * @returns true if the user should be permitted to issue invites for this room. */ public canInvite(userId: string): boolean { let canInvite = this.getMyMembership() === "join"; @@ -2808,7 +2909,7 @@ export class Room extends ReadReceipt { /** * Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`. - * @returns {string} the join_rule applied to this room + * @returns the join_rule applied to this room */ public getJoinRule(): JoinRule { return this.currentState.getJoinRule(); @@ -2816,7 +2917,7 @@ export class Room extends ReadReceipt { /** * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`. - * @returns {HistoryVisibility} the history_visibility applied to this room + * @returns the history_visibility applied to this room */ public getHistoryVisibility(): HistoryVisibility { return this.currentState.getHistoryVisibility(); @@ -2824,7 +2925,7 @@ export class Room extends ReadReceipt { /** * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`. - * @returns {HistoryVisibility} the history_visibility applied to this room + * @returns the history_visibility applied to this room */ public getGuestAccess(): GuestAccess { return this.currentState.getGuestAccess(); @@ -2832,7 +2933,7 @@ export class Room extends ReadReceipt { /** * Returns the type of the room from the `m.room.create` event content or undefined if none is set - * @returns {?string} the type of the room. + * @returns the type of the room. */ public getType(): RoomType | string | undefined { const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, ""); @@ -2848,7 +2949,7 @@ export class Room extends ReadReceipt { /** * Returns whether the room is a space-room as defined by MSC1772. - * @returns {boolean} true if the room's type is RoomType.Space + * @returns true if the room's type is RoomType.Space */ public isSpaceRoom(): boolean { return this.getType() === RoomType.Space; @@ -2856,7 +2957,7 @@ export class Room extends ReadReceipt { /** * Returns whether the room is a call-room as defined by MSC3417. - * @returns {boolean} true if the room's type is RoomType.UnstableCall + * @returns true if the room's type is RoomType.UnstableCall */ public isCallRoom(): boolean { return this.getType() === RoomType.UnstableCall; @@ -2864,7 +2965,7 @@ export class Room extends ReadReceipt { /** * Returns whether the room is a video room. - * @returns {boolean} true if the room's type is RoomType.ElementVideo + * @returns true if the room's type is RoomType.ElementVideo */ public isElementVideoRoom(): boolean { return this.getType() === RoomType.ElementVideo; @@ -2900,11 +3001,11 @@ export class Room extends ReadReceipt { /** * This is an internal method. Calculates the name of the room from the current * room state. - * @param {string} userId The client's user ID. Used to filter room members + * @param userId - The client's user ID. Used to filter room members * correctly. - * @param {boolean} ignoreRoomNameEvent Return the implicit room name that we'd see if there + * @param ignoreRoomNameEvent - Return the implicit room name that we'd see if there * was no m.room.name event. - * @return {string} The calculated room name. + * @returns The calculated room name. */ private calculateRoomName(userId: string, ignoreRoomNameEvent = false): string { if (!ignoreRoomNameEvent) { @@ -3151,7 +3252,7 @@ export class Room extends ReadReceipt { * a (more recent) visibility change event, patch the event in * place so that clients now not to display it. * - * @param event Any matrix event. If this event has at least one a + * @param event - Any matrix event. If this event has at least one a * pending visibility change event, apply the latest visibility * change event. */ @@ -3250,118 +3351,3 @@ function memberNamesToRoomName(names: string[], count: number): string { } } } - -/** - * Fires when an event we had previously received is redacted. - * - * (Note this is *not* fired when the redaction happens before we receive the - * event). - * - * @event module:client~MatrixClient#"Room.redaction" - * @param {MatrixEvent} event The matrix redaction event - * @param {Room} room The room containing the redacted event - */ - -/** - * Fires when an event that was previously redacted isn't anymore. - * This happens when the redaction couldn't be sent and - * was subsequently cancelled by the user. Redactions have a local echo - * which is undone in this scenario. - * - * @event module:client~MatrixClient#"Room.redactionCancelled" - * @param {MatrixEvent} event The matrix redaction event that was cancelled. - * @param {Room} room The room containing the unredacted event - */ - -/** - * Fires whenever the name of a room is updated. - * @event module:client~MatrixClient#"Room.name" - * @param {Room} room The room whose Room.name was updated. - * @example - * matrixClient.on("Room.name", function(room){ - * var newName = room.name; - * }); - */ - -/** - * Fires whenever a receipt is received for a room - * @event module:client~MatrixClient#"Room.receipt" - * @param {event} event The receipt event - * @param {Room} room The room whose receipts was updated. - * @example - * matrixClient.on("Room.receipt", function(event, room){ - * var receiptContent = event.getContent(); - * }); - */ - -/** - * Fires whenever a room's tags are updated. - * @event module:client~MatrixClient#"Room.tags" - * @param {event} event The tags event - * @param {Room} room The room whose Room.tags was updated. - * @example - * matrixClient.on("Room.tags", function(event, room){ - * var newTags = event.getContent().tags; - * if (newTags["favourite"]) showStar(room); - * }); - */ - -/** - * Fires whenever a room's account_data is updated. - * @event module:client~MatrixClient#"Room.accountData" - * @param {event} event The account_data event - * @param {Room} room The room whose account_data was updated. - * @param {MatrixEvent} prevEvent The event being replaced by - * the new account data, if known. - * @example - * matrixClient.on("Room.accountData", function(event, room, oldEvent){ - * if (event.getType() === "m.room.colorscheme") { - * applyColorScheme(event.getContents()); - * } - * }); - */ - -/** - * Fires when the status of a transmitted event is updated. - * - *

When an event is first transmitted, a temporary copy of the event is - * inserted into the timeline, with a temporary event id, and a status of - * 'SENDING'. - * - *

Once the echo comes back from the server, the content of the event - * (MatrixEvent.event) is replaced by the complete event from the homeserver, - * thus updating its event id, as well as server-generated fields such as the - * timestamp. Its status is set to null. - * - *

Once the /send request completes, if the remote echo has not already - * arrived, the event is updated with a new event id and the status is set to - * 'SENT'. The server-generated fields are of course not updated yet. - * - *

If the /send fails, In this case, the event's status is set to - * 'NOT_SENT'. If it is later resent, the process starts again, setting the - * status to 'SENDING'. Alternatively, the message may be cancelled, which - * removes the event from the room, and sets the status to 'CANCELLED'. - * - *

This event is raised to reflect each of the transitions above. - * - * @event module:client~MatrixClient#"Room.localEchoUpdated" - * - * @param {MatrixEvent} event The matrix event which has been updated - * - * @param {Room} room The room containing the redacted event - * - * @param {string} oldEventId The previous event id (the temporary event id, - * except when updating a successfully-sent event when its echo arrives) - * - * @param {EventStatus} oldStatus The previous event status. - */ - -/** - * Fires when the logged in user's membership in the room is updated. - * - * @event module:models/room~Room#"Room.myMembership" - * @param {Room} room The room in which the membership has been updated - * @param {string} membership The new membership value - * @param {string} prevMembership The previous membership value - */ - diff --git a/src/models/search-result.ts b/src/models/search-result.ts index f598301d785..2f27495acf4 100644 --- a/src/models/search-result.ts +++ b/src/models/search-result.ts @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module models/search-result - */ - import { EventContext } from "./event-context"; import { EventMapper } from "../event-mapper"; import { IResultContext, ISearchResult } from "../@types/search"; @@ -25,10 +21,6 @@ import { IResultContext, ISearchResult } from "../@types/search"; export class SearchResult { /** * Create a SearchResponse from the response to /search - * @static - * @param {Object} jsonObj - * @param {function} eventMapper - * @return {SearchResult} */ public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult { @@ -54,11 +46,9 @@ export class SearchResult { /** * Construct a new SearchResult * - * @param {number} rank where this SearchResult ranks in the results - * @param {event-context.EventContext} context the matching event and its + * @param rank - where this SearchResult ranks in the results + * @param context - the matching event and its * context - * - * @constructor */ public constructor(public readonly rank: number, public readonly context: EventContext) {} } diff --git a/src/models/thread.ts b/src/models/thread.ts index c595995b1cc..61b010a1859 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -247,10 +247,10 @@ export class Thread extends ReadReceipt { * Add an event to the thread and updates * the tail/root references if needed * Will fire "Thread.update" - * @param event The event to add - * @param {boolean} toStartOfTimeline whether the event is being added + * @param event - The event to add + * @param toStartOfTimeline - whether the event is being added * to the start (and not the end) of the timeline. - * @param {boolean} emit whether to emit the Update event if the thread was updated or not. + * @param emit - whether to emit the Update event if the thread was updated or not. */ public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): Promise { this.setEventMetadata(event); diff --git a/src/models/typed-event-emitter.ts b/src/models/typed-event-emitter.ts index 691ec5ec350..f9150077821 100644 --- a/src/models/typed-event-emitter.ts +++ b/src/models/typed-event-emitter.ts @@ -69,7 +69,7 @@ export class TypedEventEmitter< return super.listenerCount(event); } - public listeners(event: Events | EventEmitterEvents): Function[] { + public listeners(event: Events | EventEmitterEvents): ReturnType { return super.listeners(event); } @@ -119,7 +119,7 @@ export class TypedEventEmitter< return super.removeListener(event, listener); } - public rawListeners(event: Events | EventEmitterEvents): Function[] { + public rawListeners(event: Events | EventEmitterEvents): ReturnType { return super.rawListeners(event); } } diff --git a/src/models/user.ts b/src/models/user.ts index 5d92ca494cb..3dd199f3b1d 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module models/user - */ - import { MatrixEvent } from "./event"; import { TypedEventEmitter } from "./typed-event-emitter"; @@ -30,51 +26,132 @@ export enum UserEvent { } export type UserEventHandlerMap = { + /** + * Fires whenever any user's display name changes. + * @param event - The matrix event which caused this event to fire. + * @param user - The user whose User.displayName changed. + * @example + * ``` + * matrixClient.on("User.displayName", function(event, user){ + * var newName = user.displayName; + * }); + * ``` + */ [UserEvent.DisplayName]: (event: MatrixEvent | undefined, user: User) => void; + /** + * Fires whenever any user's avatar URL changes. + * @param event - The matrix event which caused this event to fire. + * @param user - The user whose User.avatarUrl changed. + * @example + * ``` + * matrixClient.on("User.avatarUrl", function(event, user){ + * var newUrl = user.avatarUrl; + * }); + * ``` + */ [UserEvent.AvatarUrl]: (event: MatrixEvent | undefined, user: User) => void; + /** + * Fires whenever any user's presence changes. + * @param event - The matrix event which caused this event to fire. + * @param user - The user whose User.presence changed. + * @example + * ``` + * matrixClient.on("User.presence", function(event, user){ + * var newPresence = user.presence; + * }); + * ``` + */ [UserEvent.Presence]: (event: MatrixEvent | undefined, user: User) => void; + /** + * Fires whenever any user's currentlyActive changes. + * @param event - The matrix event which caused this event to fire. + * @param user - The user whose User.currentlyActive changed. + * @example + * ``` + * matrixClient.on("User.currentlyActive", function(event, user){ + * var newCurrentlyActive = user.currentlyActive; + * }); + * ``` + */ [UserEvent.CurrentlyActive]: (event: MatrixEvent | undefined, user: User) => void; + /** + * Fires whenever any user's lastPresenceTs changes, + * ie. whenever any presence event is received for a user. + * @param event - The matrix event which caused this event to fire. + * @param user - The user whose User.lastPresenceTs changed. + * @example + * ``` + * matrixClient.on("User.lastPresenceTs", function(event, user){ + * var newlastPresenceTs = user.lastPresenceTs; + * }); + * ``` + */ [UserEvent.LastPresenceTs]: (event: MatrixEvent | undefined, user: User) => void; }; export class User extends TypedEventEmitter { private modified = -1; - // XXX these should be read-only + /** + * The 'displayname' of the user if known. + * @privateRemarks + * Should be read-only + */ public displayName?: string; public rawDisplayName?: string; + /** + * The 'avatar_url' of the user if known. + * @privateRemarks + * Should be read-only + */ public avatarUrl?: string; + /** + * The presence status message if known. + * @privateRemarks + * Should be read-only + */ public presenceStatusMsg?: string; + /** + * The presence enum if known. + * @privateRemarks + * Should be read-only + */ public presence = "offline"; + /** + * Timestamp (ms since the epoch) for when we last received presence data for this user. + * We can subtract lastActiveAgo from this to approximate an absolute value for when a user was last active. + * @privateRemarks + * Should be read-only + */ public lastActiveAgo = 0; + /** + * The time elapsed in ms since the user interacted proactively with the server, + * or we saw a message from the user + * @privateRemarks + * Should be read-only + */ public lastPresenceTs = 0; + /** + * Whether we should consider lastActiveAgo to be an approximation + * and that the user should be seen as active 'now' + * @privateRemarks + * Should be read-only + */ public currentlyActive = false; + /** + * The events describing this user. + * @privateRemarks + * Should be read-only + */ public events: { + /** The m.presence event for this user. */ presence?: MatrixEvent; profile?: MatrixEvent; } = {}; /** - * Construct a new User. A User must have an ID and can optionally have extra - * information associated with it. - * @constructor - * @param {string} userId Required. The ID of this user. - * @prop {string} userId The ID of the user. - * @prop {Object} info The info object supplied in the constructor. - * @prop {string} displayName The 'displayname' of the user if known. - * @prop {string} avatarUrl The 'avatar_url' of the user if known. - * @prop {string} presence The presence enum if known. - * @prop {string} presenceStatusMsg The presence status message if known. - * @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted - * proactively with the server, or we saw a message from the user - * @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last - * received presence data for this user. We can subtract - * lastActiveAgo from this to approximate an absolute value for - * when a user was last active. - * @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be - * an approximation and that the user should be seen as active 'now' - * @prop {Object} events The events describing this user. - * @prop {MatrixEvent} events.presence The m.presence event for this user. + * Construct a new User. A User must have an ID and can optionally have extra information associated with it. + * @param userId - Required. The ID of this user. */ public constructor(public readonly userId: string) { super(); @@ -87,10 +164,12 @@ export class User extends TypedEventEmitter { * Update this User with the given presence event. May fire "User.presence", * "User.avatarUrl" and/or "User.displayName" if this event updates this user's * properties. - * @param {MatrixEvent} event The m.presence event. - * @fires module:client~MatrixClient#event:"User.presence" - * @fires module:client~MatrixClient#event:"User.displayName" - * @fires module:client~MatrixClient#event:"User.avatarUrl" + * @param event - The `m.presence` event. + * + * @remarks + * Fires {@link UserEvent.Presence} + * Fires {@link UserEvent.DisplayName} + * Fires {@link UserEvent.AvatarUrl} */ public setPresenceEvent(event: MatrixEvent): void { if (event.getType() !== "m.presence") { @@ -142,7 +221,7 @@ export class User extends TypedEventEmitter { /** * Manually set this user's display name. No event is emitted in response to this * as there is no underlying MatrixEvent to emit with. - * @param {string} name The new display name. + * @param name - The new display name. */ public setDisplayName(name: string): void { const oldName = this.displayName; @@ -155,7 +234,7 @@ export class User extends TypedEventEmitter { /** * Manually set this user's non-disambiguated display name. No event is emitted * in response to this as there is no underlying MatrixEvent to emit with. - * @param {string} name The new display name. + * @param name - The new display name. */ public setRawDisplayName(name?: string): void { this.rawDisplayName = name; @@ -164,7 +243,7 @@ export class User extends TypedEventEmitter { /** * Manually set this user's avatar URL. No event is emitted in response to this * as there is no underlying MatrixEvent to emit with. - * @param {string} url The new avatar URL. + * @param url - The new avatar URL. */ public setAvatarUrl(url?: string): void { const oldUrl = this.avatarUrl; @@ -185,7 +264,7 @@ export class User extends TypedEventEmitter { * Get the timestamp when this User was last updated. This timestamp is * updated when this User receives a new Presence event which has updated a * property on this object. It is updated before firing events. - * @return {number} The timestamp + * @returns The timestamp */ public getLastModifiedTime(): number { return this.modified; @@ -194,65 +273,9 @@ export class User extends TypedEventEmitter { /** * Get the absolute timestamp when this User was last known active on the server. * It is *NOT* accurate if this.currentlyActive is true. - * @return {number} The timestamp + * @returns The timestamp */ public getLastActiveTs(): number { return this.lastPresenceTs - this.lastActiveAgo; } } - -/** - * Fires whenever any user's lastPresenceTs changes, - * ie. whenever any presence event is received for a user. - * @event module:client~MatrixClient#"User.lastPresenceTs" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.lastPresenceTs changed. - * @example - * matrixClient.on("User.lastPresenceTs", function(event, user){ - * var newlastPresenceTs = user.lastPresenceTs; - * }); - */ - -/** - * Fires whenever any user's presence changes. - * @event module:client~MatrixClient#"User.presence" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.presence changed. - * @example - * matrixClient.on("User.presence", function(event, user){ - * var newPresence = user.presence; - * }); - */ - -/** - * Fires whenever any user's currentlyActive changes. - * @event module:client~MatrixClient#"User.currentlyActive" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.currentlyActive changed. - * @example - * matrixClient.on("User.currentlyActive", function(event, user){ - * var newCurrentlyActive = user.currentlyActive; - * }); - */ - -/** - * Fires whenever any user's display name changes. - * @event module:client~MatrixClient#"User.displayName" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.displayName changed. - * @example - * matrixClient.on("User.displayName", function(event, user){ - * var newName = user.displayName; - * }); - */ - -/** - * Fires whenever any user's avatar URL changes. - * @event module:client~MatrixClient#"User.avatarUrl" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.avatarUrl changed. - * @example - * matrixClient.on("User.avatarUrl", function(event, user){ - * var newUrl = user.avatarUrl; - * }); - */ diff --git a/src/pushprocessor.ts b/src/pushprocessor.ts index cc124682591..e6a30769256 100644 --- a/src/pushprocessor.ts +++ b/src/pushprocessor.ts @@ -38,10 +38,6 @@ import { } from "./@types/PushRules"; import { EventType } from "./@types/event"; -/** - * @module pushprocessor - */ - const RULEKINDS_IN_ORDER = [ PushRuleKind.Override, PushRuleKind.ContentSpecific, @@ -116,25 +112,27 @@ const DEFAULT_UNDERRIDE_RULES: IPushRule[] = [ ]; export interface IActionsObject { + /** Whether this event should notify the user or not. */ notify: boolean; + /** How this event should be notified. */ tweaks: Partial>; } export class PushProcessor { /** * Construct a Push Processor. - * @constructor - * @param {Object} client The Matrix client object to use + * @param client - The Matrix client object to use */ public constructor(private readonly client: MatrixClient) {} /** * Convert a list of actions into a object with the actions as keys and their values - * eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ] - * becomes { notify: true, tweaks: { sound: 'default' } } - * @param {array} actionList The actions list + * @example + * eg. `[ 'notify', { set_tweak: 'sound', value: 'default' } ]` + * becomes `{ notify: true, tweaks: { sound: 'default' } }` + * @param actionList - The actions list * - * @return {object} A object with key 'notify' (true or false) and an object of actions + * @returns A object with key 'notify' (true or false) and an object of actions */ public static actionListToActionsObject(actionList: PushRuleAction[]): IActionsObject { const actionObj: IActionsObject = { notify: false, tweaks: {} }; @@ -155,8 +153,8 @@ export class PushProcessor { * Rewrites conditions on a client's push rules to match the defaults * where applicable. Useful for upgrading push rules to more strict * conditions when the server is falling behind on defaults. - * @param {object} incomingRules The client's existing push rules - * @returns {object} The rewritten rules + * @param incomingRules - The client's existing push rules + * @returns The rewritten rules */ public static rewriteDefaultRules(incomingRules: IPushRules): IPushRules { let newRules: IPushRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone @@ -502,10 +500,6 @@ export class PushProcessor { /** * Get the user's push actions for the given event - * - * @param {module:models/event.MatrixEvent} ev - * - * @return {PushAction} */ public actionsForEvent(ev: MatrixEvent): IActionsObject { return this.pushActionsForEventAndRulesets(ev, this.client.pushRules); @@ -514,8 +508,8 @@ export class PushProcessor { /** * Get one of the users push rules by its ID * - * @param {string} ruleId The ID of the rule to search for - * @return {object} The push rule, or null if no such rule was found + * @param ruleId - The ID of the rule to search for + * @returns The push rule, or null if no such rule was found */ public getPushRuleById(ruleId: string): IPushRule | null { for (const scope of ['global'] as const) { @@ -532,15 +526,3 @@ export class PushProcessor { return null; } } - -/** - * @typedef {Object} PushAction - * @type {Object} - * @property {boolean} notify Whether this event should notify the user or not. - * @property {Object} tweaks How this event should be notified. - * @property {boolean} tweaks.highlight Whether this event should be highlighted - * on the UI. - * @property {boolean} tweaks.sound Whether this notification should produce a - * noise. - */ - diff --git a/src/realtime-callbacks.ts b/src/realtime-callbacks.ts index 4677b0c1ed1..005e9816bc5 100644 --- a/src/realtime-callbacks.ts +++ b/src/realtime-callbacks.ts @@ -48,16 +48,17 @@ type Callback = { const callbackList: Callback[] = []; // var debuglog = logger.log.bind(logger); +/* istanbul ignore next */ const debuglog = function(...params: any[]): void {}; /** * reimplementation of window.setTimeout, which will call the callback if * the wallclock time goes past the deadline. * - * @param {function} func callback to be called after a delay - * @param {Number} delayMs number of milliseconds to delay by + * @param func - callback to be called after a delay + * @param delayMs - number of milliseconds to delay by * - * @return {Number} an identifier for this callback, which may be passed into + * @returns an identifier for this callback, which may be passed into * clearTimeout later. */ export function setTimeout(func: (...params: any[]) => void, delayMs: number, ...params: any[]): number { @@ -93,7 +94,7 @@ export function setTimeout(func: (...params: any[]) => void, delayMs: number, .. /** * reimplementation of window.clearTimeout, which mirrors setTimeout * - * @param {Number} key result from an earlier setTimeout call + * @param key - result from an earlier setTimeout call */ export function clearTimeout(key: number): void { if (callbackList.length === 0) { diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts index a93d4c476ac..d4ed1bc1709 100644 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -67,9 +67,9 @@ export class MSC3906Rendezvous { private _code?: string; /** - * @param channel The secure channel used for communication - * @param client The Matrix client in used on the device already logged in - * @param onFailure Callback for when the rendezvous fails + * @param channel - The secure channel used for communication + * @param client - The Matrix client in used on the device already logged in + * @param onFailure - Callback for when the rendezvous fails */ public constructor( private channel: RendezvousChannel, @@ -217,7 +217,7 @@ export class MSC3906Rendezvous { /** * Verify the device and cross-sign it. - * @param timeout time in milliseconds to wait for device to come online + * @param timeout - time in milliseconds to wait for device to come online * @returns the new device info if the device was verified */ public async verifyNewDeviceOnExistingDevice( diff --git a/src/rendezvous/RendezvousChannel.ts b/src/rendezvous/RendezvousChannel.ts index 9b1060304c9..f5c35217ff3 100644 --- a/src/rendezvous/RendezvousChannel.ts +++ b/src/rendezvous/RendezvousChannel.ts @@ -28,7 +28,7 @@ export interface RendezvousChannel { /** * Send a payload via the channel. - * @param data payload to send + * @param data - payload to send */ send(data: T): Promise; diff --git a/src/rendezvous/RendezvousTransport.ts b/src/rendezvous/RendezvousTransport.ts index 231a0f1c48a..8721febffa0 100644 --- a/src/rendezvous/RendezvousTransport.ts +++ b/src/rendezvous/RendezvousTransport.ts @@ -41,7 +41,7 @@ export interface RendezvousTransport { /** * Send data via the transport. - * @param data the data itself + * @param data - the data itself */ send(data: T): Promise; @@ -52,7 +52,7 @@ export interface RendezvousTransport { /** * Cancel the rendezvous. This will call `onCancelled()` if it is set. - * @param reason the reason for the cancellation/failure + * @param reason - the reason for the cancellation/failure */ cancel(reason: RendezvousFailureReason): Promise; } diff --git a/src/room-hierarchy.ts b/src/room-hierarchy.ts index c15f2ac56dc..3ed0205271a 100644 --- a/src/room-hierarchy.ts +++ b/src/room-hierarchy.ts @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** - * @module room-hierarchy - */ - import { Room } from "./models/room"; import { IHierarchyRoom, IHierarchyRelation } from "./@types/spaces"; import { MatrixClient } from "./client"; @@ -41,11 +37,10 @@ export class RoomHierarchy { * * A RoomHierarchy instance allows you to easily make use of the /hierarchy API and paginate it. * - * @param {Room} root the root of this hierarchy - * @param {number} pageSize the maximum number of rooms to return per page, can be overridden per load request. - * @param {number} maxDepth the maximum depth to traverse the hierarchy to - * @param {boolean} suggestedOnly whether to only return rooms with suggested=true. - * @constructor + * @param root - the root of this hierarchy + * @param pageSize - the maximum number of rooms to return per page, can be overridden per load request. + * @param maxDepth - the maximum depth to traverse the hierarchy to + * @param suggestedOnly - whether to only return rooms with suggested=true. */ public constructor( public readonly root: Room, diff --git a/src/scheduler.ts b/src/scheduler.ts index 74bf7dd2580..d6cfef362cf 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -17,7 +17,6 @@ limitations under the License. /** * This is an internal module which manages queuing, scheduling and retrying * of requests. - * @module scheduler */ import * as utils from "./utils"; import { logger } from './logger'; @@ -35,20 +34,13 @@ interface IQueueEntry { attempts: number; } -type ProcessFunction = (event: MatrixEvent) => Promise; - /** - * Construct a scheduler for Matrix. Requires - * {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided - * with a way of processing events. - * @constructor - * @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry - * algorithm to apply when determining when to try to send an event again. - * Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}. - * @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing - * algorithm to apply when determining which events should be sent before the - * given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}. + * The function to invoke to process (send) events in the queue. + * @param event - The event to send. + * @returns Resolved/rejected depending on the outcome of the request. */ +type ProcessFunction = (event: MatrixEvent) => Promise; + // eslint-disable-next-line camelcase export class MatrixScheduler { /** @@ -56,11 +48,8 @@ export class MatrixScheduler { * times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the * failure was due to a rate limited request, the time specified in the error is * waited before being retried. - * @param {MatrixEvent} event - * @param {Number} attempts Number of attempts that have been made, including the one that just failed (ie. starting at 1) - * @param {MatrixError} err - * @return {Number} - * @see module:scheduler~retryAlgorithm + * @param attempts - Number of attempts that have been made, including the one that just failed (ie. starting at 1) + * @see retryAlgorithm */ // eslint-disable-next-line @typescript-eslint/naming-convention public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent | null, attempts: number, err: MatrixError): number { @@ -90,11 +79,9 @@ export class MatrixScheduler { } /** - * Queues m.room.message events and lets other events continue + * Queues `m.room.message` events and lets other events continue * concurrently. - * @param {MatrixEvent} event - * @return {string} - * @see module:scheduler~queueAlgorithm + * @see queueAlgorithm */ // eslint-disable-next-line @typescript-eslint/naming-convention public static QUEUE_MESSAGES(event: MatrixEvent): string | null { @@ -116,16 +103,52 @@ export class MatrixScheduler { private activeQueues: string[] = []; private procFn: ProcessFunction | null = null; + /** + * Construct a scheduler for Matrix. Requires + * {@link MatrixScheduler#setProcessFunction} to be provided + * with a way of processing events. + * @param retryAlgorithm - Optional. The retry + * algorithm to apply when determining when to try to send an event again. + * Defaults to {@link MatrixScheduler.RETRY_BACKOFF_RATELIMIT}. + * @param queueAlgorithm - Optional. The queuing + * algorithm to apply when determining which events should be sent before the + * given event. Defaults to {@link MatrixScheduler.QUEUE_MESSAGES}. + */ public constructor( + /** + * The retry algorithm to apply when retrying events. To stop retrying, return + * `-1`. If this event was part of a queue, it will be removed from + * the queue. + * @param event - The event being retried. + * @param attempts - The number of failed attempts. This will always be \>= 1. + * @param err - The most recent error message received when trying + * to send this event. + * @returns The number of milliseconds to wait before trying again. If + * this is 0, the request will be immediately retried. If this is + * `-1`, the event will be marked as + * {@link EventStatus.NOT_SENT} and will not be retried. + */ public readonly retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT, + /** + * The queuing algorithm to apply to events. This function must be idempotent as + * it may be called multiple times with the same event. All queues created are + * serviced in a FIFO manner. To send the event ASAP, return `null` + * which will not put this event in a queue. Events that fail to send that form + * part of a queue will be removed from the queue and the next event in the + * queue will be sent. + * @param event - The event to be sent. + * @returns The name of the queue to put the event into. If a queue with + * this name does not exist, it will be created. If this is `null`, + * the event is not put into a queue and will be sent concurrently. + */ public readonly queueAlgorithm = MatrixScheduler.QUEUE_MESSAGES, ) {} /** * Retrieve a queue based on an event. The event provided does not need to be in * the queue. - * @param {MatrixEvent} event An event to get the queue for. - * @return {?Array} A shallow copy of events in the queue or null. + * @param event - An event to get the queue for. + * @returns A shallow copy of events in the queue or null. * Modifying this array will not modify the list itself. Modifying events in * this array will modify the underlying event in the queue. * @see MatrixScheduler.removeEventFromQueue To remove an event from the queue. @@ -143,8 +166,8 @@ export class MatrixScheduler { /** * Remove this event from the queue. The event is equal to another event if they * have the same ID returned from event.getId(). - * @param {MatrixEvent} event The event to remove. - * @return {boolean} True if this event was removed. + * @param event - The event to remove. + * @returns True if this event was removed. */ public removeEventFromQueue(event: MatrixEvent): boolean { const name = this.queueAlgorithm(event); @@ -168,7 +191,7 @@ export class MatrixScheduler { * Set the process function. Required for events in the queue to be processed. * If set after events have been added to the queue, this will immediately start * processing them. - * @param {module:scheduler~processFn} fn The function that can process events + * @param fn - The function that can process events * in the queue. */ public setProcessFunction(fn: ProcessFunction): void { @@ -178,8 +201,8 @@ export class MatrixScheduler { /** * Queue an event if it is required and start processing queues. - * @param {MatrixEvent} event The event that may be queued. - * @return {?Promise} A promise if the event was queued, which will be + * @param event - The event that may be queued. + * @returns A promise if the event was queued, which will be * resolved or rejected in due time, else null. */ public queueEvent(event: MatrixEvent): Promise | null { @@ -283,46 +306,9 @@ export class MatrixScheduler { } } +/* istanbul ignore next */ function debuglog(...args: any[]): void { if (DEBUG) { logger.log(...args); } } - -/** - * The retry algorithm to apply when retrying events. To stop retrying, return - * -1. If this event was part of a queue, it will be removed from - * the queue. - * @callback retryAlgorithm - * @param {MatrixEvent} event The event being retried. - * @param {Number} attempts The number of failed attempts. This will always be - * >= 1. - * @param {MatrixError} err The most recent error message received when trying - * to send this event. - * @return {Number} The number of milliseconds to wait before trying again. If - * this is 0, the request will be immediately retried. If this is - * -1, the event will be marked as - * {@link module:models/event.EventStatus.NOT_SENT} and will not be retried. - */ - -/** - * The queuing algorithm to apply to events. This function must be idempotent as - * it may be called multiple times with the same event. All queues created are - * serviced in a FIFO manner. To send the event ASAP, return null - * which will not put this event in a queue. Events that fail to send that form - * part of a queue will be removed from the queue and the next event in the - * queue will be sent. - * @callback queueAlgorithm - * @param {MatrixEvent} event The event to be sent. - * @return {string} The name of the queue to put the event into. If a queue with - * this name does not exist, it will be created. If this is null, - * the event is not put into a queue and will be sent concurrently. - */ - -/** - * The function to invoke to process (send) events in the queue. - * @callback processFn - * @param {MatrixEvent} event The event to send. - * @return {Promise} Resolved/rejected depending on the outcome of the request. - */ - diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index d103eb39bea..f9bddd87595 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -458,7 +458,7 @@ export class SlidingSyncSdk { /** * Sync rooms the user has left. - * @return {Promise} Resolved when they've been added to the store. + * @returns Resolved when they've been added to the store. */ public async syncLeftRooms(): Promise { return []; // TODO @@ -467,8 +467,8 @@ export class SlidingSyncSdk { /** * Peek into a room. This will result in the room in question being synced so it * is accessible via getRooms(). Live updates for the room will be provided. - * @param {string} roomId The room ID to peek into. - * @return {Promise} A promise which resolves once the room has been added to the + * @param roomId - The room ID to peek into. + * @returns A promise which resolves once the room has been added to the * store. */ public async peek(_roomId: string): Promise { @@ -485,8 +485,7 @@ export class SlidingSyncSdk { /** * Returns the current state of this sync object - * @see module:client~MatrixClient#event:"sync" - * @return {?String} + * @see MatrixClient#event:"sync" */ public getSyncState(): SyncState | null { return this.syncState; @@ -498,7 +497,6 @@ export class SlidingSyncSdk { * such data. * Sync errors, if available, are put in the 'error' key of * this object. - * @return {?Object} */ public getSyncStateData(): ISyncStateData | null { return this.syncStateData ?? null; @@ -765,12 +763,11 @@ export class SlidingSyncSdk { /** * Injects events into a room's model. - * @param {Room} room - * @param {MatrixEvent[]} stateEventList A list of state events. This is the state + * @param stateEventList - A list of state events. This is the state * at the *START* of the timeline list if it is supplied. - * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index + * @param timelineEventList - A list of timeline events. Lower index * is earlier in time. Higher index is later. - * @param {number} numLive the number of events in timelineEventList which just happened, + * @param numLive - the number of events in timelineEventList which just happened, * supplied from the server. */ public injectRoomEvents( @@ -933,8 +930,8 @@ export class SlidingSyncSdk { /** * Sets the sync state and emits an event to say so - * @param {String} newState The new state string - * @param {Object} data Object of additional data to emit in the event + * @param newState - The new state string + * @param data - Object of additional data to emit in the event */ private updateSyncState(newState: SyncState, data?: ISyncStateData): void { const old = this.syncState; @@ -948,7 +945,7 @@ export class SlidingSyncSdk { * as appropriate. * This must be called after the room the events belong to has been stored. * - * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index + * @param timelineEventList - A list of timeline events. Lower index * is earlier in time. Higher index is later. */ private addNotifications(timelineEventList: MatrixEvent[]): void { diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index a3daf3af6bf..81a67f05e15 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -156,7 +156,7 @@ export enum SlidingSyncState { /** * Internal Class. SlidingList represents a single list in sliding sync. The list can have filters, - * multiple sliding windows, and maintains the index->room_id mapping. + * multiple sliding windows, and maintains the index-\>room_id mapping. */ class SlidingList { private list!: MSC3575List; @@ -168,7 +168,7 @@ class SlidingList { /** * Construct a new sliding list. - * @param {MSC3575List} list The range, sort and filter values to use for this list. + * @param list - The range, sort and filter values to use for this list. */ public constructor(list: MSC3575List) { this.replaceList(list); @@ -177,7 +177,7 @@ class SlidingList { /** * Mark this list as modified or not. Modified lists will return sticky params with calls to getList. * This is useful for the first time the list is sent, or if the list has changed in some way. - * @param modified True to mark this list as modified so all sticky parameters will be re-sent. + * @param modified - True to mark this list as modified so all sticky parameters will be re-sent. */ public setModified(modified: boolean): void { this.isModified = modified; @@ -185,7 +185,7 @@ class SlidingList { /** * Update the list range for this list. Does not affect modified status as list ranges are non-sticky. - * @param newRanges The new ranges for the list + * @param newRanges - The new ranges for the list */ public updateListRange(newRanges: number[][]): void { this.list.ranges = JSON.parse(JSON.stringify(newRanges)); @@ -193,7 +193,7 @@ class SlidingList { /** * Replace list parameters. All fields will be replaced with the new list parameters. - * @param list The new list parameters + * @param list - The new list parameters */ public replaceList(list: MSC3575List): void { list.filters = list.filters || {}; @@ -213,7 +213,7 @@ class SlidingList { /** * Return a copy of the list suitable for a request body. - * @param {boolean} forceIncludeAllParams True to forcibly include all params even if the list + * @param forceIncludeAllParams - True to forcibly include all params even if the list * hasn't been modified. Callers may want to do this if they are modifying the list prior to calling * updateList. */ @@ -235,7 +235,7 @@ class SlidingList { * a b c d _ f COMMAND: DELETE 7; * e a b c d f COMMAND: INSERT 0 e; * c=3 is wrong as we are not tracking it, ergo we need to see if `i` is in range else drop it - * @param i The index to check + * @param i - The index to check * @returns True if the index is within a sliding window */ public isIndexInRange(i: number): boolean { @@ -274,13 +274,13 @@ export interface Extension { /** * A function which is called when the request JSON is being formed. * Returns the data to insert under this key. - * @param isInitial True when this is part of the initial request (send sticky params) + * @param isInitial - True when this is part of the initial request (send sticky params) * @returns The request JSON to send. */ onRequest(isInitial: boolean): Req | undefined; /** * A function which is called when there is response JSON under this extension. - * @param data The response JSON under the extension name. + * @param data - The response JSON under the extension name. */ onResponse(data: Res): void; /** @@ -368,11 +368,11 @@ export class SlidingSync extends TypedEventEmitter} | null { @@ -435,7 +435,7 @@ export class SlidingSync extends TypedEventEmitter): void { if (this.extensions[ext.name()]) { @@ -552,8 +552,8 @@ export class SlidingSync extends TypedEventEmitter void) => void; - /** @return {Promise} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ isNewlyCreated(): Promise; /** * Get the sync token. - * @return {string} */ getSyncToken(): string | null; /** * Set the sync token. - * @param {string} token */ setSyncToken(token: string): void; /** * Store the given room. - * @param {Room} room The room to be stored. All properties must be stored. + * @param room - The room to be stored. All properties must be stored. */ storeRoom(room: Room): void; /** * Retrieve a room by its' room ID. - * @param {string} roomId The room ID. - * @return {Room} The room or null. + * @param roomId - The room ID. + * @returns The room or null. */ getRoom(roomId: string): Room | null; /** * Retrieve all known rooms. - * @return {Room[]} A list of rooms, which may be empty. + * @returns A list of rooms, which may be empty. */ getRooms(): Room[]; /** * Permanently delete a room. - * @param {string} roomId */ removeRoom(roomId: string): void; /** * Retrieve a summary of all the rooms. - * @return {RoomSummary[]} A summary of each room. + * @returns A summary of each room. */ getRoomSummaries(): RoomSummary[]; /** * Store a User. - * @param {User} user The user to store. + * @param user - The user to store. */ storeUser(user: User): void; /** * Retrieve a User by its' user ID. - * @param {string} userId The user ID. - * @return {User} The user or null. + * @param userId - The user ID. + * @returns The user or null. */ getUser(userId: string): User | null; /** * Retrieve all known users. - * @return {User[]} A list of users, which may be empty. + * @returns A list of users, which may be empty. */ getUsers(): User[]; /** * Retrieve scrollback for this room. - * @param {Room} room The matrix room - * @param {number} limit The max number of old events to retrieve. - * @return {Array} An array of objects which will be at most 'limit' + * @param room - The matrix room + * @param limit - The max number of old events to retrieve. + * @returns An array of objects which will be at most 'limit' * length and at least 0. The objects are the raw event JSON. */ scrollback(room: Room, limit: number): MatrixEvent[]; /** * Store events for a room. - * @param {Room} room The room to store events for. - * @param {Array} events The events to store. - * @param {string} token The token associated with these events. - * @param {boolean} toStart True if these are paginated results. + * @param room - The room to store events for. + * @param events - The events to store. + * @param token - The token associated with these events. + * @param toStart - True if these are paginated results. */ storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void; /** * Store a filter. - * @param {Filter} filter */ storeFilter(filter: Filter): void; /** * Retrieve a filter. - * @param {string} userId - * @param {string} filterId - * @return {?Filter} A filter or null. + * @returns A filter or null. */ getFilter(userId: string, filterId: string): Filter | null; /** * Retrieve a filter ID with the given name. - * @param {string} filterName The filter name. - * @return {?string} The filter ID or null. + * @param filterName - The filter name. + * @returns The filter ID or null. */ getFilterIdByName(filterName: string): string | null; /** * Set a filter name to ID mapping. - * @param {string} filterName - * @param {string} filterId */ setFilterIdByName(filterName: string, filterId?: string): void; /** * Store user-scoped account data events - * @param {Array} events The events to store. + * @param events - The events to store. */ storeAccountDataEvents(events: MatrixEvent[]): void; /** * Get account data event by event type - * @param {string} eventType The event type being queried + * @param eventType - The event type being queried */ getAccountData(eventType: EventType | string): MatrixEvent | undefined; /** * setSyncData does nothing as there is no backing data store. * - * @param {Object} syncData The sync data - * @return {Promise} An immediately resolved promise. + * @param syncData - The sync data + * @returns An immediately resolved promise. */ setSyncData(syncData: ISyncResponse): Promise; /** * We never want to save because we have nothing to save to. * - * @return {boolean} If the store wants to save + * @returns If the store wants to save */ wantsSave(): boolean; @@ -187,19 +179,19 @@ export interface IStore { /** * Startup does nothing. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ startup(): Promise; /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ getSavedSync(): Promise; /** - * @return {Promise} If there is a saved sync, the nextBatch token + * @returns If there is a saved sync, the nextBatch token * for this sync, otherwise null. */ getSavedSyncToken(): Promise; @@ -207,16 +199,15 @@ export interface IStore { /** * Delete all data from this store. Does nothing since this store * doesn't store anything. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ deleteAllData(): Promise; /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ getOutOfBandMembers(roomId: string): Promise; @@ -224,9 +215,8 @@ export interface IStore { * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store - * @returns {Promise} when all members have been stored + * @param membershipEvents - the membership events to store + * @returns when all members have been stored */ setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise; diff --git a/src/store/indexeddb-local-backend.ts b/src/store/indexeddb-local-backend.ts index 2b94af25600..fe61f11df37 100644 --- a/src/store/indexeddb-local-backend.ts +++ b/src/store/indexeddb-local-backend.ts @@ -51,12 +51,12 @@ const VERSION = DB_MIGRATIONS.length; /** * Helper method to collect results from a Cursor and promiseify it. - * @param {ObjectStore|Index} store The store to perform openCursor on. - * @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor. - * @param {Function} resultMapper A function which is repeatedly called with a + * @param store - The store to perform openCursor on. + * @param keyRange - Optional key range to apply on the cursor. + * @param resultMapper - A function which is repeatedly called with a * Cursor. * Return the data you want to keep. - * @return {Promise} Resolves to an array of whatever you returned from + * @returns Promise which resolves to an array of whatever you returned from * resultMapper. */ function selectQuery( @@ -134,11 +134,10 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * Does the actual reading from and writing to the indexeddb * * Construct a new Indexed Database store backend. This requires a call to - * connect() before this store can be used. - * @constructor - * @param {Object} indexedDB The Indexed DB interface e.g - * window.indexedDB - * @param {string=} dbName Optional database name. The same name must be used + * `connect()` before this store can be used. + * @param indexedDB - The Indexed DB interface e.g + * `window.indexedDB` + * @param dbName - Optional database name. The same name must be used * to open the same database. */ public constructor(private readonly indexedDB: IDBFactory, dbName = "default") { @@ -149,7 +148,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Attempt to connect to the database. This can fail if the user does not * grant permission. - * @return {Promise} Resolves if successfully connected. + * @returns Promise which resolves if successfully connected. */ public connect(): Promise { if (!this.disconnected) { @@ -195,14 +194,14 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { }); } - /** @return {boolean} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ public isNewlyCreated(): Promise { return Promise.resolve(this._isNewlyCreated); } /** * Having connected, load initial data from the database and prepare for use - * @return {Promise} Resolves on success + * @returns Promise which resolves on success */ private init(): Promise { return Promise.all([ @@ -223,9 +222,8 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {Promise} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ public getOutOfBandMembers(roomId: string): Promise { return new Promise((resolve, reject) => { @@ -273,8 +271,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store + * @param membershipEvents - the membership events to store */ public async setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise { logger.log(`LL: backend about to store ${membershipEvents.length}` + @@ -339,7 +336,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Clear the entire database. This should be used when logging out of a client * to prevent mixing data between accounts. - * @return {Promise} Resolved when the database is cleared. + * @returns Resolved when the database is cleared. */ public clearDatabase(): Promise { return new Promise((resolve) => { @@ -366,11 +363,11 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { } /** - * @param {boolean=} copy If false, the data returned is from internal + * @param copy - If false, the data returned is from internal * buffers and must not be mutated. Otherwise, a copy is made before * returning such that the data can be safely mutated. Default: true. * - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ @@ -421,9 +418,9 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Persist rooms /sync data along with the next batch token. - * @param {string} nextBatch The next_batch /sync value. - * @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator - * @return {Promise} Resolves if the data was persisted. + * @param nextBatch - The next_batch /sync value. + * @param roomsData - The 'rooms' /sync data from a SyncAccumulator + * @returns Promise which resolves if the data was persisted. */ private persistSyncData( nextBatch: string, @@ -447,8 +444,8 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Persist a list of account data events. Events with the same 'type' will * be replaced. - * @param {Object[]} accountData An array of raw user-scoped account data events - * @return {Promise} Resolves if the events were persisted. + * @param accountData - An array of raw user-scoped account data events + * @returns Promise which resolves if the events were persisted. */ private persistAccountData(accountData: IMinimalEvent[]): Promise { return utils.promiseTry(() => { @@ -466,8 +463,8 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * Users with the same 'userId' will be replaced. * Presence events should be the event in its raw form (not the Event * object) - * @param {Object[]} tuples An array of [userid, event] tuples - * @return {Promise} Resolves if the users were persisted. + * @param tuples - An array of [userid, event] tuples + * @returns Promise which resolves if the users were persisted. */ private persistUserPresenceEvents(tuples: UserTuple[]): Promise { return utils.promiseTry(() => { @@ -487,7 +484,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { * Load all user presence events from the database. This is not cached. * FIXME: It would probably be more sensible to store the events in the * sync. - * @return {Promise} A list of presence events in their raw form. + * @returns A list of presence events in their raw form. */ public getUserPresenceEvents(): Promise { return utils.promiseTry(() => { @@ -501,7 +498,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Load all the account data events from the database. This is not cached. - * @return {Promise} A list of raw global account events. + * @returns A list of raw global account events. */ private loadAccountData(): Promise { logger.log(`LocalIndexedDBStoreBackend: loading account data...`); @@ -519,7 +516,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { /** * Load the sync data from the database. - * @return {Promise} An object with "roomsData" and "nextBatch" keys. + * @returns An object with "roomsData" and "nextBatch" keys. */ private loadSyncData(): Promise { logger.log(`LocalIndexedDBStoreBackend: loading sync data...`); diff --git a/src/store/indexeddb-remote-backend.ts b/src/store/indexeddb-remote-backend.ts index 1ca6fc03a38..05909b0e3f5 100644 --- a/src/store/indexeddb-remote-backend.ts +++ b/src/store/indexeddb-remote-backend.ts @@ -36,10 +36,9 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { * worker. * * Construct a new Indexed Database store backend. This requires a call to - * connect() before this store can be used. - * @constructor - * @param {Function} workerFactory Factory which produces a Worker - * @param {string=} dbName Optional database name. The same name must be used + * `connect()` before this store can be used. + * @param workerFactory - Factory which produces a Worker + * @param dbName - Optional database name. The same name must be used * to open the same database. */ public constructor( @@ -50,7 +49,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { /** * Attempt to connect to the database. This can fail if the user does not * grant permission. - * @return {Promise} Resolves if successfully connected. + * @returns Promise which resolves if successfully connected. */ public connect(): Promise { return this.ensureStarted().then(() => this.doCmd('connect')); @@ -59,19 +58,19 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { /** * Clear the entire database. This should be used when logging out of a client * to prevent mixing data between accounts. - * @return {Promise} Resolved when the database is cleared. + * @returns Resolved when the database is cleared. */ public clearDatabase(): Promise { return this.ensureStarted().then(() => this.doCmd('clearDatabase')); } - /** @return {Promise} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ public isNewlyCreated(): Promise { return this.doCmd('isNewlyCreated'); } /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ @@ -94,9 +93,8 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ public getOutOfBandMembers(roomId: string): Promise { return this.doCmd('getOutOfBandMembers', [roomId]); @@ -106,9 +104,8 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store - * @returns {Promise} when all members have been stored + * @param membershipEvents - the membership events to store + * @returns when all members have been stored */ public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise { return this.doCmd('setOutOfBandMembers', [roomId, membershipEvents]); @@ -128,7 +125,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { /** * Load all user presence events from the database. This is not cached. - * @return {Promise} A list of presence events in their raw form. + * @returns A list of presence events in their raw form. */ public getUserPresenceEvents(): Promise { return this.doCmd('getUserPresenceEvents'); diff --git a/src/store/indexeddb-store-worker.ts b/src/store/indexeddb-store-worker.ts index 57e7da983be..a9bc2abcd07 100644 --- a/src/store/indexeddb-store-worker.ts +++ b/src/store/indexeddb-store-worker.ts @@ -27,12 +27,14 @@ interface ICmd { * This class lives in the webworker and drives a LocalIndexedDBStoreBackend * controlled by messages from the main process. * + * @example * It should be instantiated by a web worker script provided by the application * in a script, for example: - * + * ``` * import {IndexedDBStoreWorker} from 'matrix-js-sdk/lib/indexeddb-worker.js'; * const remoteWorker = new IndexedDBStoreWorker(postMessage); * onmessage = remoteWorker.onMessage; + * ``` * * Note that it is advisable to import this class by referencing the file directly to * avoid a dependency on the whole js-sdk. @@ -42,7 +44,7 @@ export class IndexedDBStoreWorker { private backend?: LocalIndexedDBStoreBackend; /** - * @param {function} postMessage The web worker postMessage function that + * @param postMessage - The web worker postMessage function that * should be used to communicate back to the main script. */ public constructor(private readonly postMessage: InstanceType["postMessage"]) {} @@ -51,7 +53,7 @@ export class IndexedDBStoreWorker { * Passes a message event from the main script into the class. This method * can be directly assigned to the web worker `onmessage` variable. * - * @param {Object} ev The message event + * @param ev - The message event */ public onMessage = (ev: MessageEvent): void => { const msg: ICmd = ev.data; diff --git a/src/store/indexeddb.ts b/src/store/indexeddb.ts index 55f8261faa8..656049f2b27 100644 --- a/src/store/indexeddb.ts +++ b/src/store/indexeddb.ts @@ -32,7 +32,6 @@ import { IStoredClientOpts } from "../client"; /** * This is an internal module. See {@link IndexedDBStore} for the public class. - * @module store/indexeddb */ // If this value is too small we'll be writing very often which will cause @@ -43,8 +42,11 @@ import { IStoredClientOpts } from "../client"; const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes interface IOpts extends IBaseOpts { + /** The Indexed DB interface e.g. `window.indexedDB` */ indexedDB: IDBFactory; + /** Optional database name. The same name must be used to open the same database. */ dbName?: string; + /** Optional factory to spin up a Worker to execute the IDB transactions within. */ workerFactory?: () => Worker; } @@ -57,6 +59,10 @@ export class IndexedDBStore extends MemoryStore { return LocalIndexedDBStoreBackend.exists(indexedDB, dbName); } + /** + * The backend instance. + * Call through to this API if you need to perform specific indexeddb actions like deleting the database. + */ public readonly backend: IIndexedDBBackend; private startedUp = false; @@ -74,10 +80,10 @@ export class IndexedDBStore extends MemoryStore { * the contents of the store to an IndexedDB backend. * * All data is still kept in-memory but can be loaded from disk by calling - * startup(). This can make startup times quicker as a complete + * `startup()`. This can make startup times quicker as a complete * sync from the server is not required. This does not reduce memory usage as all - * the data is eagerly fetched when startup() is called. - *
+     * the data is eagerly fetched when `startup()` is called.
+     * ```
      * let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage };
      * let store = new IndexedDBStore(opts);
      * await store.startup(); // load from indexed db
@@ -90,24 +96,9 @@ export class IndexedDBStore extends MemoryStore {
      *         console.log("Started up, now with go faster stripes!");
      *     }
      * });
-     * 
+ * ``` * - * @constructor - * @extends MemoryStore - * @param {Object} opts Options object. - * @param {Object} opts.indexedDB The Indexed DB interface e.g. - * window.indexedDB - * @param {string=} opts.dbName Optional database name. The same name must be used - * to open the same database. - * @param {string=} opts.workerScript Optional URL to a script to invoke a web - * worker with to run IndexedDB queries on the web worker. The IndexedDbStoreWorker - * class is provided for this purpose and requires the application to provide a - * trivial wrapper script around it. - * @param {Object=} opts.workerApi The webWorker API object. If omitted, the global Worker - * object will be used if it exists. - * @prop {IndexedDBStoreBackend} backend The backend instance. Call through to - * this API if you need to perform specific indexeddb actions like deleting the - * database. + * @param opts - Options object. */ public constructor(opts: IOpts) { super(opts); @@ -126,7 +117,7 @@ export class IndexedDBStore extends MemoryStore { public on = this.emitter.on.bind(this.emitter); /** - * @return {Promise} Resolved when loaded from indexed db. + * @returns Resolved when loaded from indexed db. */ public startup(): Promise { if (this.startedUp) { @@ -152,7 +143,7 @@ export class IndexedDBStore extends MemoryStore { } /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ @@ -160,13 +151,13 @@ export class IndexedDBStore extends MemoryStore { return this.backend.getSavedSync(); }, "getSavedSync"); - /** @return {Promise} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ public isNewlyCreated = this.degradable((): Promise => { return this.backend.isNewlyCreated(); }, "isNewlyCreated"); /** - * @return {Promise} If there is a saved sync, the nextBatch token + * @returns If there is a saved sync, the nextBatch token * for this sync, otherwise null. */ public getSavedSyncToken = this.degradable((): Promise => { @@ -175,7 +166,7 @@ export class IndexedDBStore extends MemoryStore { /** * Delete all data from this store. - * @return {Promise} Resolves if the data was deleted from the database. + * @returns Promise which resolves if the data was deleted from the database. */ public deleteAllData = this.degradable((): Promise => { super.deleteAllData(); @@ -193,7 +184,7 @@ export class IndexedDBStore extends MemoryStore { * not could change between calling this function and calling * save(). * - * @return {boolean} True if calling save() will actually save + * @returns True if calling save() will actually save * (at the time this function is called). */ public wantsSave(): boolean { @@ -204,8 +195,8 @@ export class IndexedDBStore extends MemoryStore { /** * Possibly write data to the database. * - * @param {boolean} force True to force a save to happen - * @return {Promise} Promise resolves after the write completes + * @param force - True to force a save to happen + * @returns Promise resolves after the write completes * (or immediately if no write is performed) */ public save(force = false): Promise { @@ -241,9 +232,8 @@ export class IndexedDBStore extends MemoryStore { /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ public getOutOfBandMembers = this.degradable((roomId: string): Promise => { return this.backend.getOutOfBandMembers(roomId); @@ -253,9 +243,8 @@ export class IndexedDBStore extends MemoryStore { * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store - * @returns {Promise} when all members have been stored + * @param membershipEvents - the membership events to store + * @returns when all members have been stored */ public setOutOfBandMembers = this.degradable( (roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise => { @@ -287,9 +276,9 @@ export class IndexedDBStore extends MemoryStore { * When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore` * in place so that the current operation and all future ones are in-memory only. * - * @param {Function} func The degradable work to do. - * @param {String} fallback The method name for fallback. - * @returns {Function} A wrapped member function. + * @param func - The degradable work to do. + * @param fallback - The method name for fallback. + * @returns A wrapped member function. */ private degradable, R = void>( func: DegradableFn, @@ -368,8 +357,8 @@ export class IndexedDBStore extends MemoryStore { } /** - * @param {string} roomId ID of the current room - * @returns {string} Storage key to retrieve pending events + * @param roomId - ID of the current room + * @returns Storage key to retrieve pending events */ function pendingEventsKey(roomId: string): string { return `mx_pending_events_${roomId}`; diff --git a/src/store/memory.ts b/src/store/memory.ts index 1fcb6b0c865..c698b93955f 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -16,7 +16,6 @@ limitations under the License. /** * This is an internal module. See {@link MemoryStore} for the public class. - * @module store/memory */ import { EventType } from "../@types/event"; @@ -43,16 +42,10 @@ function isValidFilterId(filterId?: string | number | null): boolean { } export interface IOpts { + /** The local storage instance to persist some forms of data such as tokens. Rooms will NOT be stored. */ localStorage?: Storage; } -/** - * Construct a new in-memory data store for the Matrix Client. - * @constructor - * @param {Object=} opts Config options - * @param {Storage} opts.localStorage The local storage instance to persist - * some forms of data such as tokens. Rooms will NOT be stored. - */ export class MemoryStore implements IStore { private rooms: Record = {}; // roomId: Room private users: Record = {}; // userId: User @@ -69,26 +62,30 @@ export class MemoryStore implements IStore { private pendingToDeviceBatches: IndexedToDeviceBatch[] = []; private nextToDeviceBatchId = 0; + /** + * Construct a new in-memory data store for the Matrix Client. + * @param opts - Config options + */ public constructor(opts: IOpts = {}) { this.localStorage = opts.localStorage; } /** * Retrieve the token to stream from. - * @return {string} The token or null. + * @returns The token or null. */ public getSyncToken(): string | null { return this.syncToken; } - /** @return {Promise} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ public isNewlyCreated(): Promise { return Promise.resolve(true); } /** * Set the token to stream from. - * @param {string} token The token to stream from. + * @param token - The token to stream from. */ public setSyncToken(token: string): void { this.syncToken = token; @@ -96,7 +93,7 @@ export class MemoryStore implements IStore { /** * Store the given room. - * @param {Room} room The room to be stored. All properties must be stored. + * @param room - The room to be stored. All properties must be stored. */ public storeRoom(room: Room): void { this.rooms[room.roomId] = room; @@ -112,9 +109,6 @@ export class MemoryStore implements IStore { /** * Called when a room member in a room being tracked by this store has been * updated. - * @param {MatrixEvent} event - * @param {RoomState} state - * @param {RoomMember} member */ private onRoomMember = (event: MatrixEvent | null, state: RoomState, member: RoomMember): void => { if (member.membership === "invite") { @@ -138,8 +132,8 @@ export class MemoryStore implements IStore { /** * Retrieve a room by its' room ID. - * @param {string} roomId The room ID. - * @return {Room} The room or null. + * @param roomId - The room ID. + * @returns The room or null. */ public getRoom(roomId: string): Room | null { return this.rooms[roomId] || null; @@ -147,7 +141,7 @@ export class MemoryStore implements IStore { /** * Retrieve all known rooms. - * @return {Room[]} A list of rooms, which may be empty. + * @returns A list of rooms, which may be empty. */ public getRooms(): Room[] { return Object.values(this.rooms); @@ -155,7 +149,6 @@ export class MemoryStore implements IStore { /** * Permanently delete a room. - * @param {string} roomId */ public removeRoom(roomId: string): void { if (this.rooms[roomId]) { @@ -166,7 +159,7 @@ export class MemoryStore implements IStore { /** * Retrieve a summary of all the rooms. - * @return {RoomSummary[]} A summary of each room. + * @returns A summary of each room. */ public getRoomSummaries(): RoomSummary[] { return Object.values(this.rooms).map(function(room) { @@ -176,7 +169,7 @@ export class MemoryStore implements IStore { /** * Store a User. - * @param {User} user The user to store. + * @param user - The user to store. */ public storeUser(user: User): void { this.users[user.userId] = user; @@ -184,8 +177,8 @@ export class MemoryStore implements IStore { /** * Retrieve a User by its' user ID. - * @param {string} userId The user ID. - * @return {User} The user or null. + * @param userId - The user ID. + * @returns The user or null. */ public getUser(userId: string): User | null { return this.users[userId] || null; @@ -193,7 +186,7 @@ export class MemoryStore implements IStore { /** * Retrieve all known users. - * @return {User[]} A list of users, which may be empty. + * @returns A list of users, which may be empty. */ public getUsers(): User[] { return Object.values(this.users); @@ -201,9 +194,9 @@ export class MemoryStore implements IStore { /** * Retrieve scrollback for this room. - * @param {Room} room The matrix room - * @param {number} limit The max number of old events to retrieve. - * @return {Array} An array of objects which will be at most 'limit' + * @param room - The matrix room + * @param limit - The max number of old events to retrieve. + * @returns An array of objects which will be at most 'limit' * length and at least 0. The objects are the raw event JSON. */ public scrollback(room: Room, limit: number): MatrixEvent[] { @@ -212,10 +205,10 @@ export class MemoryStore implements IStore { /** * Store events for a room. The events have already been added to the timeline - * @param {Room} room The room to store events for. - * @param {Array} events The events to store. - * @param {string} token The token associated with these events. - * @param {boolean} toStart True if these are paginated results. + * @param room - The room to store events for. + * @param events - The events to store. + * @param token - The token associated with these events. + * @param toStart - True if these are paginated results. */ public storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void { // no-op because they've already been added to the room instance. @@ -223,7 +216,6 @@ export class MemoryStore implements IStore { /** * Store a filter. - * @param {Filter} filter */ public storeFilter(filter: Filter): void { if (!filter?.userId || !filter?.filterId) return; @@ -235,9 +227,7 @@ export class MemoryStore implements IStore { /** * Retrieve a filter. - * @param {string} userId - * @param {string} filterId - * @return {?Filter} A filter or null. + * @returns A filter or null. */ public getFilter(userId: string, filterId: string): Filter | null { if (!this.filters[userId] || !this.filters[userId][filterId]) { @@ -248,8 +238,8 @@ export class MemoryStore implements IStore { /** * Retrieve a filter ID with the given name. - * @param {string} filterName The filter name. - * @return {?string} The filter ID or null. + * @param filterName - The filter name. + * @returns The filter ID or null. */ public getFilterIdByName(filterName: string): string | null { if (!this.localStorage) { @@ -272,8 +262,6 @@ export class MemoryStore implements IStore { /** * Set a filter name to ID mapping. - * @param {string} filterName - * @param {string} filterId */ public setFilterIdByName(filterName: string, filterId?: string): void { if (!this.localStorage) { @@ -293,7 +281,7 @@ export class MemoryStore implements IStore { * Store user-scoped account data events. * N.B. that account data only allows a single event per type, so multiple * events with the same type will replace each other. - * @param {Array} events The events to store. + * @param events - The events to store. */ public storeAccountDataEvents(events: MatrixEvent[]): void { events.forEach((event) => { @@ -303,8 +291,8 @@ export class MemoryStore implements IStore { /** * Get account data event by event type - * @param {string} eventType The event type being queried - * @return {?MatrixEvent} the user account_data event of given type, if any + * @param eventType - The event type being queried + * @returns the user account_data event of given type, if any */ public getAccountData(eventType: EventType | string): MatrixEvent | undefined { return this.accountData[eventType]; @@ -313,8 +301,8 @@ export class MemoryStore implements IStore { /** * setSyncData does nothing as there is no backing data store. * - * @param {Object} syncData The sync data - * @return {Promise} An immediately resolved promise. + * @param syncData - The sync data + * @returns An immediately resolved promise. */ public setSyncData(syncData: ISyncResponse): Promise { return Promise.resolve(); @@ -323,7 +311,7 @@ export class MemoryStore implements IStore { /** * We never want to save becase we have nothing to save to. * - * @return {boolean} If the store wants to save + * @returns If the store wants to save */ public wantsSave(): boolean { return false; @@ -331,21 +319,21 @@ export class MemoryStore implements IStore { /** * Save does nothing as there is no backing data store. - * @param {bool} force True to force a save (but the memory + * @param force - True to force a save (but the memory * store still can't save anything) */ public save(force: boolean): void {} /** * Startup does nothing as this store doesn't require starting up. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ public startup(): Promise { return Promise.resolve(); } /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ @@ -354,7 +342,7 @@ export class MemoryStore implements IStore { } /** - * @return {Promise} If there is a saved sync, the nextBatch token + * @returns If there is a saved sync, the nextBatch token * for this sync, otherwise null. */ public getSavedSyncToken(): Promise { @@ -363,7 +351,7 @@ export class MemoryStore implements IStore { /** * Delete all data from this store. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ public deleteAllData(): Promise { this.rooms = { @@ -387,9 +375,8 @@ export class MemoryStore implements IStore { /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ public getOutOfBandMembers(roomId: string): Promise { return Promise.resolve(this.oobMembers[roomId] || null); @@ -399,9 +386,8 @@ export class MemoryStore implements IStore { * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store - * @returns {Promise} when all members have been stored + * @param membershipEvents - the membership events to store + * @returns when all members have been stored */ public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise { this.oobMembers[roomId] = membershipEvents; diff --git a/src/store/stub.ts b/src/store/stub.ts index 74234393879..445f9e8ff27 100644 --- a/src/store/stub.ts +++ b/src/store/stub.ts @@ -16,7 +16,6 @@ limitations under the License. /** * This is an internal module. - * @module store/stub */ import { EventType } from "../@types/event"; @@ -33,20 +32,18 @@ import { IStoredClientOpts } from "../client"; /** * Construct a stub store. This does no-ops on most store methods. - * @constructor */ export class StubStore implements IStore { public readonly accountData = {}; // stub private fromToken: string | null = null; - /** @return {Promise} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ public isNewlyCreated(): Promise { return Promise.resolve(true); } /** * Get the sync token. - * @return {string} */ public getSyncToken(): string | null { return this.fromToken; @@ -54,7 +51,6 @@ export class StubStore implements IStore { /** * Set the sync token. - * @param {string} token */ public setSyncToken(token: string): void { this.fromToken = token; @@ -62,14 +58,11 @@ export class StubStore implements IStore { /** * No-op. - * @param {Room} room */ public storeRoom(room: Room): void {} /** * No-op. - * @param {string} roomId - * @return {null} */ public getRoom(roomId: string): Room | null { return null; @@ -77,7 +70,7 @@ export class StubStore implements IStore { /** * No-op. - * @return {Array} An empty array. + * @returns An empty array. */ public getRooms(): Room[] { return []; @@ -85,7 +78,6 @@ export class StubStore implements IStore { /** * Permanently delete a room. - * @param {string} roomId */ public removeRoom(roomId: string): void { return; @@ -93,7 +85,7 @@ export class StubStore implements IStore { /** * No-op. - * @return {Array} An empty array. + * @returns An empty array. */ public getRoomSummaries(): RoomSummary[] { return []; @@ -101,14 +93,11 @@ export class StubStore implements IStore { /** * No-op. - * @param {User} user */ public storeUser(user: User): void {} /** * No-op. - * @param {string} userId - * @return {null} */ public getUser(userId: string): User | null { return null; @@ -116,7 +105,6 @@ export class StubStore implements IStore { /** * No-op. - * @return {User[]} */ public getUsers(): User[] { return []; @@ -124,9 +112,6 @@ export class StubStore implements IStore { /** * No-op. - * @param {Room} room - * @param {number} limit - * @return {Array} */ public scrollback(room: Room, limit: number): MatrixEvent[] { return []; @@ -134,24 +119,21 @@ export class StubStore implements IStore { /** * Store events for a room. - * @param {Room} room The room to store events for. - * @param {Array} events The events to store. - * @param {string} token The token associated with these events. - * @param {boolean} toStart True if these are paginated results. + * @param room - The room to store events for. + * @param events - The events to store. + * @param token - The token associated with these events. + * @param toStart - True if these are paginated results. */ public storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void {} /** * Store a filter. - * @param {Filter} filter */ public storeFilter(filter: Filter): void {} /** * Retrieve a filter. - * @param {string} userId - * @param {string} filterId - * @return {?Filter} A filter or null. + * @returns A filter or null. */ public getFilter(userId: string, filterId: string): Filter | null { return null; @@ -159,8 +141,8 @@ export class StubStore implements IStore { /** * Retrieve a filter ID with the given name. - * @param {string} filterName The filter name. - * @return {?string} The filter ID or null. + * @param filterName - The filter name. + * @returns The filter ID or null. */ public getFilterIdByName(filterName: string): string | null { return null; @@ -168,20 +150,18 @@ export class StubStore implements IStore { /** * Set a filter name to ID mapping. - * @param {string} filterName - * @param {string} filterId */ public setFilterIdByName(filterName: string, filterId?: string): void {} /** * Store user-scoped account data events - * @param {Array} events The events to store. + * @param events - The events to store. */ public storeAccountDataEvents(events: MatrixEvent[]): void {} /** * Get account data event by event type - * @param {string} eventType The event type being queried + * @param eventType - The event type being queried */ public getAccountData(eventType: EventType | string): MatrixEvent | undefined { return undefined; @@ -190,8 +170,8 @@ export class StubStore implements IStore { /** * setSyncData does nothing as there is no backing data store. * - * @param {Object} syncData The sync data - * @return {Promise} An immediately resolved promise. + * @param syncData - The sync data + * @returns An immediately resolved promise. */ public setSyncData(syncData: ISyncResponse): Promise { return Promise.resolve(); @@ -200,7 +180,7 @@ export class StubStore implements IStore { /** * We never want to save because we have nothing to save to. * - * @return {boolean} If the store wants to save + * @returns If the store wants to save */ public wantsSave(): boolean { return false; @@ -213,14 +193,14 @@ export class StubStore implements IStore { /** * Startup does nothing. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ public startup(): Promise { return Promise.resolve(); } /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ @@ -229,7 +209,7 @@ export class StubStore implements IStore { } /** - * @return {Promise} If there is a saved sync, the nextBatch token + * @returns If there is a saved sync, the nextBatch token * for this sync, otherwise null. */ public getSavedSyncToken(): Promise { @@ -239,7 +219,7 @@ export class StubStore implements IStore { /** * Delete all data from this store. Does nothing since this store * doesn't store anything. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ public deleteAllData(): Promise { return Promise.resolve(); diff --git a/src/sync-accumulator.ts b/src/sync-accumulator.ts index f383cb98a4a..13f87ecc06e 100644 --- a/src/sync-accumulator.ts +++ b/src/sync-accumulator.ts @@ -16,7 +16,6 @@ limitations under the License. /** * This is an internal module. See {@link SyncAccumulator} for the public class. - * @module sync-accumulator */ import { logger } from './logger'; @@ -28,6 +27,13 @@ import { MAIN_ROOM_TIMELINE, ReceiptContent, ReceiptType } from "./@types/read_r import { UNREAD_THREAD_NOTIFICATIONS } from './@types/sync'; interface IOpts { + /** + * The ideal maximum number of timeline entries to keep in the sync response. + * This is best-effort, as clients do not always have a back-pagination token for each event, + * so it's possible there may be slightly *less* than this value. There will never be more. + * This cannot be 0 or else it makes it impossible to scroll back in a room. + * Default: 50. + */ maxTimelineEntries?: number; } @@ -211,15 +217,6 @@ export class SyncAccumulator { // streaming from without losing events. private nextBatch: string | null = null; - /** - * @param {Object} opts - * @param {Number=} opts.maxTimelineEntries The ideal maximum number of - * timeline entries to keep in the sync response. This is best-effort, as - * clients do not always have a back-pagination token for each event, so - * it's possible there may be slightly *less* than this value. There will - * never be more. This cannot be 0 or else it makes it impossible to scroll - * back in a room. Default: 50. - */ public constructor(private readonly opts: IOpts = {}) { this.opts.maxTimelineEntries = this.opts.maxTimelineEntries || 50; } @@ -242,8 +239,8 @@ export class SyncAccumulator { /** * Accumulate incremental /sync room data. - * @param {Object} syncResponse the complete /sync JSON - * @param {boolean} fromDatabase True if the sync response is one saved to the database + * @param syncResponse - the complete /sync JSON + * @param fromDatabase - True if the sync response is one saved to the database */ private accumulateRooms(syncResponse: ISyncResponse, fromDatabase = false): void { if (!syncResponse.rooms) { @@ -531,8 +528,8 @@ export class SyncAccumulator { * represents all room data that should be stored. This should be paired * with the sync token which represents the most recent /sync response * provided to accumulate(). - * @param {boolean} forDatabase True to generate a sync to be saved to storage - * @return {Object} An object with a "nextBatch", "roomsData" and "accountData" + * @param forDatabase - True to generate a sync to be saved to storage + * @returns An object with a "nextBatch", "roomsData" and "accountData" * keys. * The "nextBatch" key is a string which represents at what point in the * /sync stream the accumulator reached. This token should be used when diff --git a/src/sync.ts b/src/sync.ts index c97ba35401d..38325ca8340 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -106,6 +106,7 @@ function getFilterName(userId: string, suffix?: string): string { return `FILTER_SYNC_${userId}` + (suffix ? "_" + suffix : ""); } +/* istanbul ignore next */ function debuglog(...params: any[]): void { if (!DEBUG) return; logger.log(...params); @@ -117,9 +118,27 @@ interface ISyncOptions { } export interface ISyncStateData { + /** + * The matrix error if `state=ERROR`. + */ error?: Error; + /** + * The 'since' token passed to /sync. + * `null` for the first successful sync since this client was + * started. Only present if `state=PREPARED` or + * `state=SYNCING`. + */ oldSyncToken?: string; + /** + * The 'next_batch' result from /sync, which + * will become the 'since' token for the next call to /sync. Only present if + * `state=PREPARED or state=SYNCING`. + */ nextSyncToken?: string; + /** + * True if we are working our way through a + * backlog of events after connecting. Only present if `state=SYNCING`. + */ catchingUp?: boolean; fromCache?: boolean; } @@ -146,21 +165,6 @@ type WrappedRoom = T & { isBrandNewRoom: boolean; }; -/** - * Internal class - unstable. - * Construct an entity which is able to sync with a homeserver. - * @constructor - * @param {MatrixClient} client The matrix client instance to use. - * @param {Object} opts Config options - * @param {module:crypto=} opts.crypto Crypto manager - * @param {Function=} opts.canResetEntireTimeline A function which is called - * with a room ID and returns a boolean. It should return 'true' if the SDK can - * SAFELY remove events from this room. It may not be safe to remove events if - * there are other references to the timelines for this room. - * Default: returns false. - * @param {Boolean=} opts.disablePresence True to perform syncing without automatically - * updating presence. - */ export class SyncApi { private _peekRoom: Optional = null; private currentSyncRequest?: Promise; @@ -175,6 +179,12 @@ export class SyncApi { private failedSyncCount = 0; // Number of consecutive failed /sync requests private storeIsInvalid = false; // flag set if the store needs to be cleared before we can start + /** + * Construct an entity which is able to sync with a homeserver. + * @param client - The matrix client instance to use. + * @param opts - Config options + * @internal + */ public constructor(private readonly client: MatrixClient, private readonly opts: Partial = {}) { this.opts.initialSyncLimit = this.opts.initialSyncLimit ?? 8; this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false; @@ -196,10 +206,6 @@ export class SyncApi { } } - /** - * @param {string} roomId - * @return {Room} - */ public createRoom(roomId: string): Room { const room = _createAndReEmitRoom(this.client, roomId, this.opts); @@ -214,9 +220,9 @@ export class SyncApi { * new historical messages imported by MSC2716 `/batch_send` somewhere in * the room and we need to throw away the timeline to make sure the * historical messages are shown when we paginate `/messages` again. - * @param {Room} room The room where the marker event was sent - * @param {MatrixEvent} markerEvent The new marker event - * @param {IMarkerFoundOptions} setStateOptions When `timelineWasEmpty` is set + * @param room - The room where the marker event was sent + * @param markerEvent - The new marker event + * @param setStateOptions - When `timelineWasEmpty` is set * as `true`, the given marker event will be ignored */ private onMarkerStateEvent( @@ -279,7 +285,7 @@ export class SyncApi { /** * Sync rooms the user has left. - * @return {Promise} Resolved when they've been added to the store. + * @returns Resolved when they've been added to the store. */ public async syncLeftRooms(): Promise { const client = this.client; @@ -350,8 +356,8 @@ export class SyncApi { /** * Peek into a room. This will result in the room in question being synced so it * is accessible via getRooms(). Live updates for the room will be provided. - * @param {string} roomId The room ID to peek into. - * @return {Promise} A promise which resolves once the room has been added to the + * @param roomId - The room ID to peek into. + * @returns A promise which resolves once the room has been added to the * store. */ public peek(roomId: string): Promise { @@ -430,8 +436,7 @@ export class SyncApi { /** * Do a peek room poll. - * @param {Room} peekRoom - * @param {string?} token from= token + * @param token - from= token */ private peekPoll(peekRoom: Room, token?: string): void { if (this._peekRoom !== peekRoom) { @@ -494,8 +499,7 @@ export class SyncApi { /** * Returns the current state of this sync object - * @see module:client~MatrixClient#event:"sync" - * @return {?String} + * @see MatrixClient#event:"sync" */ public getSyncState(): SyncState | null { return this.syncState; @@ -507,7 +511,6 @@ export class SyncApi { * such data. * Sync errors, if available, are put in the 'error' key of * this object. - * @return {?Object} */ public getSyncStateData(): ISyncStateData | null { return this.syncStateData ?? null; @@ -526,8 +529,8 @@ export class SyncApi { /** * Is the lazy loading option different than in previous session? - * @param {boolean} lazyLoadMembers current options for lazy loading - * @return {boolean} whether or not the option has changed compared to the previous session */ + * @param lazyLoadMembers - current options for lazy loading + * @returns whether or not the option has changed compared to the previous session */ private async wasLazyLoadingToggled(lazyLoadMembers = false): Promise { // assume it was turned off before // if we don't know any better @@ -758,7 +761,7 @@ export class SyncApi { /** * Retry a backed off syncing request immediately. This should only be used when * the user explicitly attempts to retry their lost connection. - * @return {boolean} True if this resulted in a request being retried. + * @returns True if this resulted in a request being retried. */ public retryImmediately(): boolean { if (!this.connectionReturnedDefer) { @@ -769,7 +772,7 @@ export class SyncApi { } /** * Process a single set of cached sync data. - * @param {Object} savedSync a saved sync that was persisted by a store. This + * @param savedSync - a saved sync that was persisted by a store. This * should have been acquired via client.store.getSavedSync(). */ private async syncFromCache(savedSync: ISavedSync): Promise { @@ -811,9 +814,6 @@ export class SyncApi { /** * Invoke me to do /sync calls - * @param {Object} syncOptions - * @param {string} syncOptions.filterId - * @param {boolean} syncOptions.hasSyncedBefore */ private async doSync(syncOptions: ISyncOptions): Promise { while (this.running) { @@ -1023,8 +1023,8 @@ export class SyncApi { * Process data returned from a sync response and propagate it * into the model objects * - * @param {Object} syncEventData Object containing sync tokens associated with this sync - * @param {Object} data The response from /sync + * @param syncEventData - Object containing sync tokens associated with this sync + * @param data - The response from /sync */ private async processSyncResponse(syncEventData: ISyncStateData, data: ISyncResponse): Promise { const client = this.client; @@ -1490,10 +1490,10 @@ export class SyncApi { /** * Starts polling the connectivity check endpoint - * @param {number} delay How long to delay until the first poll. + * @param delay - How long to delay until the first poll. * defaults to a short, randomised interval (to prevent * tight-looping if /versions succeeds but /sync etc. fail). - * @return {promise} which resolves once the connection returns + * @returns which resolves once the connection returns */ private startKeepAlives(delay?: number): Promise { if (delay === undefined) { @@ -1521,7 +1521,7 @@ export class SyncApi { * On failure, schedules a call back to itself. On success, resolves * this.connectionReturnedDefer. * - * @param {boolean} connDidFail True if a connectivity failure has been detected. Optional. + * @param connDidFail - True if a connectivity failure has been detected. Optional. */ private pokeKeepAlive(connDidFail = false): void { const success = (): void => { @@ -1568,10 +1568,6 @@ export class SyncApi { }); } - /** - * @param {Object} obj - * @return {Object[]} - */ private mapSyncResponseToRoomArray( obj: Record, ): Array> { @@ -1593,12 +1589,6 @@ export class SyncApi { }); } - /** - * @param {Object} obj - * @param {Room} room - * @param {boolean} decrypt - * @return {MatrixEvent[]} - */ private mapSyncEventsFormat( obj: IInviteState | ITimeline | IEphemeral, room?: Room, @@ -1618,7 +1608,6 @@ export class SyncApi { } /** - * @param {Room} room */ private resolveInvites(room: Room): void { if (!room || !this.opts.resolveInvitesToProfiles) { @@ -1662,12 +1651,11 @@ export class SyncApi { /** * Injects events into a room's model. - * @param {Room} room - * @param {MatrixEvent[]} stateEventList A list of state events. This is the state + * @param stateEventList - A list of state events. This is the state * at the *START* of the timeline list if it is supplied. - * @param {MatrixEvent[]} [timelineEventList] A list of timeline events, including threaded. Lower index + * @param timelineEventList - A list of timeline events, including threaded. Lower index * is earlier in time. Higher index is later. - * @param {boolean} fromCache whether the sync response came from cache + * @param fromCache - whether the sync response came from cache */ public async injectRoomEvents( room: Room, @@ -1743,8 +1731,7 @@ export class SyncApi { * as appropriate. * This must be called after the room the events belong to has been stored. * - * @param {Room} room - * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index + * @param timelineEventList - A list of timeline events. Lower index * is earlier in time. Higher index is later. */ private processEventsForNotifs(room: Room, timelineEventList: MatrixEvent[]): void { @@ -1759,9 +1746,6 @@ export class SyncApi { } } - /** - * @return {string} - */ private getGuestFilter(): string { // Dev note: This used to be conditional to return a filter of 20 events maximum, but // the condition never went to the other branch. This is now hardcoded. @@ -1770,8 +1754,8 @@ export class SyncApi { /** * Sets the sync state and emits an event to say so - * @param {String} newState The new state string - * @param {Object} data Object of additional data to emit in the event + * @param newState - The new state string + * @param data - Object of additional data to emit in the event */ private updateSyncState(newState: SyncState, data?: ISyncStateData): void { const old = this.syncState; diff --git a/src/timeline-window.ts b/src/timeline-window.ts index 5498600a99c..2d0c9da15de 100644 --- a/src/timeline-window.ts +++ b/src/timeline-window.ts @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** @module timeline-window */ - import { Optional } from "matrix-events-sdk"; import { Direction, EventTimeline } from './models/event-timeline'; @@ -25,23 +23,28 @@ import { EventTimelineSet } from "./models/event-timeline-set"; import { MatrixEvent } from "./models/event"; /** - * @private + * @internal */ const DEBUG = false; /** - * @private + * @internal */ +/* istanbul ignore next */ const debuglog = DEBUG ? logger.log.bind(logger) : function(): void {}; /** * the number of times we ask the server for more events before giving up * - * @private + * @internal */ const DEFAULT_PAGINATE_LOOP_LIMIT = 5; interface IOpts { + /** + * Maximum number of events to keep in the window. If more events are retrieved via pagination requests, + * excess events will be dropped from the other end of the window. + */ windowLimit?: number; } @@ -58,31 +61,22 @@ export class TimelineWindow { /** * Construct a TimelineWindow. * - *

This abstracts the separate timelines in a Matrix {@link - * module:models/room|Room} into a single iterable thing. It keeps track of - * the start and endpoints of the window, which can be advanced with the help + *

This abstracts the separate timelines in a Matrix {@link Room} into a single iterable thing. + * It keeps track of the start and endpoints of the window, which can be advanced with the help * of pagination requests. * - *

Before the window is useful, it must be initialised by calling {@link - * module:timeline-window~TimelineWindow#load|load}. + *

Before the window is useful, it must be initialised by calling {@link TimelineWindow#load}. * *

Note that the window will not automatically extend itself when new events - * are received from /sync; you should arrange to call {@link - * module:timeline-window~TimelineWindow#paginate|paginate} on {@link - * module:client~MatrixClient.event:"Room.timeline"|Room.timeline} events. + * are received from /sync; you should arrange to call {@link TimelineWindow#paginate} + * on {@link RoomEvent.Timeline} events. * - * @param {MatrixClient} client MatrixClient to be used for context/pagination + * @param client - MatrixClient to be used for context/pagination * requests. * - * @param {EventTimelineSet} timelineSet The timelineSet to track - * - * @param {Object} [opts] Configuration options for this window + * @param timelineSet - The timelineSet to track * - * @param {number} [opts.windowLimit = 1000] maximum number of events to keep - * in the window. If more events are retrieved via pagination requests, - * excess events will be dropped from the other end of the window. - * - * @constructor + * @param opts - Configuration options for this window */ public constructor( private readonly client: MatrixClient, @@ -95,11 +89,9 @@ export class TimelineWindow { /** * Initialise the window to point at a given event, or the live timeline * - * @param {string} [initialEventId] If given, the window will contain the + * @param initialEventId - If given, the window will contain the * given event - * @param {number} [initialWindowSize = 20] Size of the initial window - * - * @return {Promise} + * @param initialWindowSize - Size of the initial window */ public load(initialEventId?: string, initialWindowSize = 20): Promise { // given an EventTimeline, find the event we were looking for, and initialise our @@ -148,11 +140,11 @@ export class TimelineWindow { /** * Get the TimelineIndex of the window in the given direction. * - * @param {string} direction EventTimeline.BACKWARDS to get the TimelineIndex + * @param direction - EventTimeline.BACKWARDS to get the TimelineIndex * at the start of the window; EventTimeline.FORWARDS to get the TimelineIndex at * the end. * - * @return {TimelineIndex} The requested timeline index if one exists, null + * @returns The requested timeline index if one exists, null * otherwise. */ public getTimelineIndex(direction: Direction): TimelineIndex | null { @@ -169,11 +161,11 @@ export class TimelineWindow { * Try to extend the window using events that are already in the underlying * TimelineIndex. * - * @param {string} direction EventTimeline.BACKWARDS to try extending it + * @param direction - EventTimeline.BACKWARDS to try extending it * backwards; EventTimeline.FORWARDS to try extending it forwards. - * @param {number} size number of events to try to extend by. + * @param size - number of events to try to extend by. * - * @return {boolean} true if the window was extended, false otherwise. + * @returns true if the window was extended, false otherwise. */ public extend(direction: Direction, size: number): boolean { const tl = this.getTimelineIndex(direction); @@ -209,10 +201,10 @@ export class TimelineWindow { * necessarily mean that there are more events available in that direction at * this time. * - * @param {string} direction EventTimeline.BACKWARDS to check if we can + * @param direction - EventTimeline.BACKWARDS to check if we can * paginate backwards; EventTimeline.FORWARDS to check if we can go forwards * - * @return {boolean} true if we can paginate in the given direction + * @returns true if we can paginate in the given direction */ public canPaginate(direction: Direction): boolean { const tl = this.getTimelineIndex(direction); @@ -240,23 +232,23 @@ export class TimelineWindow { /** * Attempt to extend the window * - * @param {string} direction EventTimeline.BACKWARDS to extend the window + * @param direction - EventTimeline.BACKWARDS to extend the window * backwards (towards older events); EventTimeline.FORWARDS to go forwards. * - * @param {number} size number of events to try to extend by. If fewer than this + * @param size - number of events to try to extend by. If fewer than this * number are immediately available, then we return immediately rather than * making an API call. * - * @param {boolean} [makeRequest = true] whether we should make API calls to + * @param makeRequest - whether we should make API calls to * fetch further events if we don't have any at all. (This has no effect if * the room already knows about additional events in the relevant direction, * even if there are fewer than 'size' of them, as we will just return those * we already know about.) * - * @param {number} [requestLimit = 5] limit for the number of API requests we + * @param requestLimit - limit for the number of API requests we * should make. * - * @return {Promise} Resolves to a boolean which is true if more events + * @returns Promise which resolves to a boolean which is true if more events * were successfully retrieved. */ public async paginate( @@ -330,8 +322,8 @@ export class TimelineWindow { /** * Remove `delta` events from the start or end of the timeline. * - * @param {number} delta number of events to remove from the timeline - * @param {boolean} startOfTimeline if events should be removed from the start + * @param delta - number of events to remove from the timeline + * @param startOfTimeline - if events should be removed from the start * of the timeline. */ public unpaginate(delta: number, startOfTimeline: boolean): void { @@ -368,7 +360,7 @@ export class TimelineWindow { /** * Get a list of the events currently in the window * - * @return {MatrixEvent[]} the events in the window + * @returns the events in the window */ public getEvents(): MatrixEvent[] { if (!this.start) { @@ -419,12 +411,8 @@ export class TimelineWindow { } /** - * a thing which contains a timeline reference, and an index into it. - * - * @constructor - * @param {EventTimeline} timeline - * @param {number} index - * @private + * A thing which contains a timeline reference, and an index into it. + * @internal */ export class TimelineIndex { public pendingPaginate?: Promise; @@ -433,7 +421,7 @@ export class TimelineIndex { public constructor(public timeline: EventTimeline, public index: number) {} /** - * @return {number} the minimum possible value for the index in the current + * @returns the minimum possible value for the index in the current * timeline */ public minIndex(): number { @@ -441,7 +429,7 @@ export class TimelineIndex { } /** - * @return {number} the maximum possible value for the index in the current + * @returns the maximum possible value for the index in the current * timeline (exclusive - ie, it actually returns one more than the index * of the last element). */ @@ -452,8 +440,8 @@ export class TimelineIndex { /** * Try move the index forward, or into the neighbouring timeline * - * @param {number} delta number of events to advance by - * @return {number} number of events successfully advanced by + * @param delta - number of events to advance by + * @returns number of events successfully advanced by */ public advance(delta: number): number { if (!delta) { @@ -512,8 +500,8 @@ export class TimelineIndex { /** * Try move the index backwards, or into the neighbouring timeline * - * @param {number} delta number of events to retreat by - * @return {number} number of events successfully retreated by + * @param delta - number of events to retreat by + * @returns number of events successfully retreated by */ public retreat(delta: number): number { return this.advance(delta * -1) * -1; diff --git a/src/utils.ts b/src/utils.ts index ef6af6b0668..8cf248c783d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -17,7 +17,6 @@ limitations under the License. /** * This is an internal module. - * @module utils */ import unhomoglyph from "unhomoglyph"; @@ -33,7 +32,7 @@ const interns = new Map(); /** * Internalises a string, reusing a known pointer or storing the pointer * if needed for future strings. - * @param str The string to internalise. + * @param str - The string to internalise. * @returns The internalised string. */ export function internaliseString(str: string): string { @@ -55,9 +54,9 @@ export function internaliseString(str: string): string { /** * Encode a dictionary of query parameters. * Omits any undefined/null values. - * @param {Object} params A dict of key/values to encode e.g. - * {"foo": "bar", "baz": "taz"} - * @return {string} The encoded string e.g. foo=bar&baz=taz + * @param params - A dict of key/values to encode e.g. + * `{"foo": "bar", "baz": "taz"}` + * @returns The encoded string e.g. foo=bar&baz=taz */ export function encodeParams(params: QueryDict, urlSearchParams?: URLSearchParams): URLSearchParams { const searchParams = urlSearchParams ?? new URLSearchParams(); @@ -79,9 +78,6 @@ export type QueryDict = Record { /** * Encodes a URI according to a set of template variables. Variables will be * passed through encodeURIComponent. - * @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'. - * @param {Object} variables The key/value pairs to replace the template - * variables with. E.g. { "$bar": "baz" }. - * @return {string} The result of replacing all template variables e.g. '/foo/baz'. + * @param pathTemplate - The path with template variables e.g. '/foo/$bar'. + * @param variables - The key/value pairs to replace the template + * variables with. E.g. `{ "$bar": "baz" }`. + * @returns The result of replacing all template variables e.g. '/foo/baz'. */ export function encodeUri(pathTemplate: string, variables: Record>): string { for (const key in variables) { @@ -142,12 +138,12 @@ export function encodeUri(pathTemplate: string, variables: Recordfn(element, index, array). Return true to + * @param array - The array. + * @param fn - Function to execute on each value in the array, with the + * function signature `fn(element, index, array)`. Return true to * remove this element and break. - * @param {boolean} reverse True to search in reverse order. - * @return {boolean} True if an element was removed. + * @param reverse - True to search in reverse order. + * @returns True if an element was removed. */ export function removeElement( array: T[], @@ -175,8 +171,8 @@ export function removeElement( /** * Checks if the given thing is a function. - * @param {*} value The thing to check. - * @return {boolean} True if it is a function. + * @param value - The thing to check. + * @returns True if it is a function. */ export function isFunction(value: any): boolean { return Object.prototype.toString.call(value) === "[object Function]"; @@ -184,8 +180,8 @@ export function isFunction(value: any): boolean { /** * Checks that the given object has the specified keys. - * @param {Object} obj The object to check. - * @param {string[]} keys The list of keys that 'obj' must have. + * @param obj - The object to check. + * @param keys - The list of keys that 'obj' must have. * @throws If the object is missing keys. */ // note using 'keys' here would shadow the 'keys' function defined above @@ -200,8 +196,8 @@ export function checkObjectHasKeys(obj: object, keys: string[]): void { /** * Deep copy the given object. The object MUST NOT have circular references and * MUST NOT have functions. - * @param {Object} obj The object to deep copy. - * @return {Object} A copy of the object without any references to the original. + * @param obj - The object to deep copy. + * @returns A copy of the object without any references to the original. */ export function deepCopy(obj: T): T { return JSON.parse(JSON.stringify(obj)); @@ -210,10 +206,10 @@ export function deepCopy(obj: T): T { /** * Compare two objects for equality. The objects MUST NOT have circular references. * - * @param {Object} x The first object to compare. - * @param {Object} y The second object to compare. + * @param x - The first object to compare. + * @param y - The second object to compare. * - * @return {boolean} true if the two objects are equal + * @returns true if the two objects are equal */ export function deepCompare(x: any, y: any): boolean { // Inspired by @@ -290,8 +286,8 @@ export function deepCompare(x: any, y: any): boolean { * sorts the result by key, recursively. The input object must * ensure it does not have loops. If the input is not an object * then it will be returned as-is. - * @param {*} obj The object to get entries of - * @returns {Array} The entries, sorted by key. + * @param obj - The object to get entries of + * @returns The entries, sorted by key. */ export function deepSortedObjectEntries(obj: any): [string, any][] { if (typeof(obj) !== "object") return obj; @@ -313,8 +309,8 @@ export function deepSortedObjectEntries(obj: any): [string, any][] { /** * Returns whether the given value is a finite number without type-coercion * - * @param {*} value the value to test - * @return {boolean} whether or not value is a finite number without type-coercion + * @param value - the value to test + * @returns whether or not value is a finite number without type-coercion */ export function isNumber(value: any): value is number { return typeof value === 'number' && isFinite(value); @@ -323,8 +319,8 @@ export function isNumber(value: any): value is number { /** * Removes zero width chars, diacritics and whitespace from the string * Also applies an unhomoglyph on the string, to prevent similar looking chars - * @param {string} str the string to remove hidden characters from - * @return {string} a string with the hidden characters removed + * @param str - the string to remove hidden characters from + * @returns a string with the hidden characters removed */ export function removeHiddenChars(str: string): string { if (typeof str === "string") { @@ -335,7 +331,6 @@ export function removeHiddenChars(str: string): string { /** * Removes the direction override characters from a string - * @param {string} input * @returns string with chars removed */ export function removeDirectionOverrideChars(str: string): string { @@ -466,9 +461,9 @@ export async function chunkPromises(fns: (() => Promise)[], chunkSize: num * a promise which throws/rejects on error, otherwise the retry will assume the request * succeeded. The promise chain returned will contain the successful promise. The given function * should always return a new promise. - * @param {Function} promiseFn The function to call to get a fresh promise instance. Takes an + * @param promiseFn - The function to call to get a fresh promise instance. Takes an * attempt count as an argument, for logging/debugging purposes. - * @returns {Promise} The promise for the retried operation. + * @returns The promise for the retried operation. */ export function simpleRetryOperation(promiseFn: (attempt: number) => Promise): Promise { return promiseRetry((attempt: number) => { @@ -503,10 +498,10 @@ export const DEFAULT_ALPHABET = ((): string => { * padded at the end with the first character in the alphabet. * * This is intended for use with string averaging. - * @param {string} s The string to pad. - * @param {number} n The length to pad to. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The padded string. + * @param s - The string to pad. + * @param n - The length to pad to. + * @param alphabet - The alphabet to use as a single string. + * @returns The padded string. */ export function alphabetPad(s: string, n: number, alphabet = DEFAULT_ALPHABET): string { return s.padEnd(n, alphabet[0]); @@ -516,9 +511,9 @@ export function alphabetPad(s: string, n: number, alphabet = DEFAULT_ALPHABET): * Converts a baseN number to a string, where N is the alphabet's length. * * This is intended for use with string averaging. - * @param {bigint} n The baseN number. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The baseN number encoded as a string from the alphabet. + * @param n - The baseN number. + * @param alphabet - The alphabet to use as a single string. + * @returns The baseN number encoded as a string from the alphabet. */ export function baseToString(n: bigint, alphabet = DEFAULT_ALPHABET): string { // Developer note: the stringToBase() function offsets the character set by 1 so that repeated @@ -550,9 +545,9 @@ export function baseToString(n: bigint, alphabet = DEFAULT_ALPHABET): string { * Converts a string to a baseN number, where N is the alphabet's length. * * This is intended for use with string averaging. - * @param {string} s The string to convert to a number. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {bigint} The baseN number. + * @param s - The string to convert to a number. + * @param alphabet - The alphabet to use as a single string. + * @returns The baseN number. */ export function stringToBase(s: string, alphabet = DEFAULT_ALPHABET): bigint { const len = BigInt(alphabet.length); @@ -584,10 +579,10 @@ export function stringToBase(s: string, alphabet = DEFAULT_ALPHABET): bigint { * Averages two strings, returning the midpoint between them. This is accomplished by * converting both to baseN numbers (where N is the alphabet's length) then averaging * those before re-encoding as a string. - * @param {string} a The first string. - * @param {string} b The second string. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The midpoint between the strings, as a string. + * @param a - The first string. + * @param b - The second string. + * @param alphabet - The alphabet to use as a single string. + * @returns The midpoint between the strings, as a string. */ export function averageBetweenStrings(a: string, b: string, alphabet = DEFAULT_ALPHABET): string { const padN = Math.max(a.length, b.length); @@ -608,9 +603,9 @@ export function averageBetweenStrings(a: string, b: string, alphabet = DEFAULT_A * Finds the next string using the alphabet provided. This is done by converting the * string to a baseN number, where N is the alphabet's length, then adding 1 before * converting back to a string. - * @param {string} s The string to start at. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The string which follows the input string. + * @param s - The string to start at. + * @param alphabet - The alphabet to use as a single string. + * @returns The string which follows the input string. */ export function nextString(s: string, alphabet = DEFAULT_ALPHABET): string { return baseToString(stringToBase(s, alphabet) + BigInt(1), alphabet); @@ -620,9 +615,9 @@ export function nextString(s: string, alphabet = DEFAULT_ALPHABET): string { * Finds the previous string using the alphabet provided. This is done by converting the * string to a baseN number, where N is the alphabet's length, then subtracting 1 before * converting back to a string. - * @param {string} s The string to start at. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The string which precedes the input string. + * @param s - The string to start at. + * @param alphabet - The alphabet to use as a single string. + * @returns The string which precedes the input string. */ export function prevString(s: string, alphabet = DEFAULT_ALPHABET): string { return baseToString(stringToBase(s, alphabet) - BigInt(1), alphabet); @@ -630,9 +625,9 @@ export function prevString(s: string, alphabet = DEFAULT_ALPHABET): string { /** * Compares strings lexicographically as a sort-safe function. - * @param {string} a The first (reference) string. - * @param {string} b The second (compare) string. - * @returns {number} Negative if the reference string is before the compare string; + * @param a - The first (reference) string. + * @param b - The second (compare) string. + * @returns Negative if the reference string is before the compare string; * positive if the reference string is after; and zero if equal. */ export function lexicographicCompare(a: string, b: string): number { @@ -650,8 +645,8 @@ export function lexicographicCompare(a: string, b: string): number { const collator = new Intl.Collator(); /** * Performant language-sensitive string comparison - * @param a the first string to compare - * @param b the second string to compare + * @param a - the first string to compare + * @param b - the second string to compare */ export function compare(a: string, b: string): number { return collator.compare(a, b); @@ -661,8 +656,6 @@ export function compare(a: string, b: string): number { * This function is similar to Object.assign() but it assigns recursively and * allows you to ignore nullish values from the source * - * @param {Object} target - * @param {Object} source * @returns the target object */ export function recursivelyAssign>( @@ -701,7 +694,7 @@ export function isSupportedReceiptType(receiptType: string): boolean { /** * Determines whether two maps are equal. - * @param eq The equivalence relation to compare values by. Defaults to strict equality. + * @param eq - The equivalence relation to compare values by. Defaults to strict equality. */ export function mapsEqual(x: Map, y: Map, eq = (v1: V, v2: V): boolean => v1 === v2): boolean { if (x.size !== y.size) return false; diff --git a/src/webrtc/audioContext.ts b/src/webrtc/audioContext.ts index 0e08574b8c8..7cf3ed3f66e 100644 --- a/src/webrtc/audioContext.ts +++ b/src/webrtc/audioContext.ts @@ -22,7 +22,7 @@ let refCount = 0; * It's highly recommended to reuse this AudioContext rather than creating your * own, because multiple AudioContexts can be problematic in some browsers. * Make sure to call releaseContext when you're done using it. - * @returns {AudioContext} The shared AudioContext + * @returns The shared AudioContext */ export const acquireContext = (): AudioContext => { if (audioContext === null) audioContext = new AudioContext(); diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 7d303af4a5c..c9bcac1124c 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -19,7 +19,6 @@ limitations under the License. /** * This is an internal module. See {@link createNewMatrixCall} for the public API. - * @module webrtc/call */ import { v4 as uuidv4 } from "uuid"; @@ -54,29 +53,19 @@ import { GroupCallUnknownDeviceError } from './groupCall'; import { IScreensharingOpts } from "./mediaHandler"; import { MatrixError } from "../http-api"; -// events: hangup, error(err), replaced(call), state(state, oldState) - -/** - * Fires whenever an error occurs when call.js encounters an issue with setting up the call. - *

- * The error given will have a code equal to either `MatrixCall.ERR_LOCAL_OFFER_FAILED` or - * `MatrixCall.ERR_NO_USER_MEDIA`. `ERR_LOCAL_OFFER_FAILED` is emitted when the local client - * fails to create an offer. `ERR_NO_USER_MEDIA` is emitted when the user has denied access - * to their audio/video hardware. - * - * @event module:webrtc/call~MatrixCall#"error" - * @param {Error} err The error raised by MatrixCall. - * @example - * matrixCall.on("error", function(err){ - * console.error(err.code, err); - * }); - */ - interface CallOpts { + // The room ID for this call. roomId?: string; invitee?: string; + // The Matrix Client instance to send events to. client: MatrixClient; + /** + * Whether relay through TURN should be forced. + * @deprecated use opts.forceTURN when creating the matrix client + * since it's only possible to set this option on outbound calls. + */ forceTURN?: boolean; + // A list of TURN servers. turnServers?: Array; opponentDeviceId?: string; opponentSessionId?: string; @@ -320,17 +309,6 @@ function getTransceiverKey(purpose: SDPStreamMetadataPurpose, kind: TransceiverK return purpose + ':' + kind; } -/** - * Construct a new Matrix Call. - * @constructor - * @param {Object} opts Config options. - * @param {string} opts.roomId The room ID for this call. - * @param {Object} opts.webRtc The WebRTC globals from the browser. - * @param {boolean} opts.forceTURN whether relay through TURN should be forced. - * @param {Object} opts.URL The URL global. - * @param {Array} opts.turnServers Optional. A list of TURN servers. - * @param {MatrixClient} opts.client The Matrix Client instance to send events to. - */ export class MatrixCall extends TypedEventEmitter { public roomId?: string; public callId: string; @@ -405,6 +383,10 @@ export class MatrixCall extends TypedEventEmitter} CallFeeds + * @returns CallFeeds */ public getFeeds(): Array { return this.feeds; @@ -565,7 +547,7 @@ export class MatrixCall extends TypedEventEmitter} local CallFeeds + * @returns local CallFeeds */ public getLocalFeeds(): Array { return this.feeds.filter((feed) => feed.isLocal()); @@ -573,7 +555,7 @@ export class MatrixCall extends TypedEventEmitter} remote CallFeeds + * @returns remote CallFeeds */ public getRemoteFeeds(): Array { return this.feeds.filter((feed) => !feed.isLocal()); @@ -605,7 +587,7 @@ export class MatrixCall extends TypedEventEmitter !feed.isLocal()); @@ -747,8 +729,8 @@ export class MatrixCall extends TypedEventEmitter callFeed.stream.id === feed.stream.id)) { @@ -820,7 +802,7 @@ export class MatrixCall extends TypedEventEmitter { const invite = event.getContent(); @@ -970,7 +952,7 @@ export class MatrixCall extends TypedEventEmitter { // Skip if there is nothing to do @@ -1233,9 +1215,9 @@ export class MatrixCall extends TypedEventEmitter { @@ -1371,7 +1353,7 @@ export class MatrixCall extends TypedEventEmitterall of the tracks need to be muted * for this to return true. This means if there are no video tracks, this will * return true. - * @return {Boolean} True if the local preview video is muted, else false + * @returns True if the local preview video is muted, else false * (including if the call is not set up yet). */ public isLocalVideoMuted(): boolean { @@ -1380,7 +1362,7 @@ export class MatrixCall extends TypedEventEmitter { @@ -1405,7 +1387,7 @@ export class MatrixCall extends TypedEventEmitterall of the tracks need to be muted * for this to return true. This means if there are no audio tracks, this will * return true. - * @return {Boolean} True if the mic is muted, else false (including if the call + * @returns True if the mic is muted, else false (including if the call * is not set up yet). */ public isMicrophoneMuted(): boolean { @@ -1459,7 +1441,7 @@ export class MatrixCall extends TypedEventEmitter { if (event.candidate) { @@ -1752,7 +1733,6 @@ export class MatrixCall extends TypedEventEmitter { const content = event.getContent(); @@ -2258,10 +2238,7 @@ export class MatrixCall extends TypedEventEmitter { const realContent = Object.assign({}, content, { @@ -2321,7 +2298,7 @@ export class MatrixCall extends TypedEventEmitter void; }; diff --git a/src/webrtc/callFeed.ts b/src/webrtc/callFeed.ts index 14abc30d22a..3ed84a58620 100644 --- a/src/webrtc/callFeed.ts +++ b/src/webrtc/callFeed.ts @@ -189,7 +189,7 @@ export class CallFeed extends TypedEventEmitter /** * Returns true if CallFeed is local, otherwise returns false - * @returns {boolean} is local? + * @returns is local? */ public isLocal(): boolean { return this.userId === this.client.getUserId() @@ -199,7 +199,7 @@ export class CallFeed extends TypedEventEmitter /** * Returns true if audio is muted or if there are no audio * tracks, otherwise returns false - * @returns {boolean} is audio muted? + * @returns is audio muted? */ public isAudioMuted(): boolean { return this.stream.getAudioTracks().length === 0 || this.audioMuted; @@ -208,7 +208,7 @@ export class CallFeed extends TypedEventEmitter /** * Returns true video is muted or if there are no video * tracks, otherwise returns false - * @returns {boolean} is video muted? + * @returns is video muted? */ public isVideoMuted(): boolean { // We assume only one video track @@ -224,7 +224,7 @@ export class CallFeed extends TypedEventEmitter * The stream will be different and new stream as remore parties are * concerned, but this can be used for convenience locally to set up * volume listeners automatically on the new stream etc. - * @param newStream new stream with which to replace the current one + * @param newStream - new stream with which to replace the current one */ public setNewStream(newStream: MediaStream): void { this.updateStream(this.stream, newStream); @@ -233,8 +233,8 @@ export class CallFeed extends TypedEventEmitter /** * Set one or both of feed's internal audio and video video mute state * Either value may be null to leave it as-is - * @param audioMuted is the feed's audio muted? - * @param videoMuted is the feed's video muted? + * @param audioMuted - is the feed's audio muted? + * @param videoMuted - is the feed's video muted? */ public setAudioVideoMuted(audioMuted: boolean | null, videoMuted: boolean | null): void { if (audioMuted !== null) { @@ -249,7 +249,7 @@ export class CallFeed extends TypedEventEmitter /** * Starts emitting volume_changed events where the emitter value is in decibels - * @param enabled emit volume changes + * @param enabled - emit volume changes */ public measureVolumeActivity(enabled: boolean): void { if (enabled) { diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index 5941b6a3728..51e1a0a7835 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -63,6 +63,21 @@ export type GroupCallEventHandlerMap = { ) => void; [GroupCallEvent.LocalMuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void; [GroupCallEvent.ParticipantsChanged]: (participants: Map>) => void; + /** + * Fires whenever an error occurs when call.js encounters an issue with setting up the call. + *

+ * The error given will have a code equal to either `MatrixCall.ERR_LOCAL_OFFER_FAILED` or + * `MatrixCall.ERR_NO_USER_MEDIA`. `ERR_LOCAL_OFFER_FAILED` is emitted when the local client + * fails to create an offer. `ERR_NO_USER_MEDIA` is emitted when the user has denied access + * to their audio/video hardware. + * @param err - The error raised by MatrixCall. + * @example + * ``` + * matrixCall.on("error", function(err){ + * console.error(err.code, err); + * }); + * ``` + */ [GroupCallEvent.Error]: (error: GroupCallError) => void; }; @@ -302,7 +317,7 @@ export class GroupCall extends TypedEventEmitter< /** * Executes the given callback on all calls in this group call. - * @param f The callback. + * @param f - The callback. */ public forEachCall(f: (call: MatrixCall) => void): void { for (const deviceMap of this.calls.values()) { @@ -512,8 +527,8 @@ export class GroupCall extends TypedEventEmitter< /** * Sets the mute state of the local participants's microphone. - * @param {boolean} muted Whether to mute the microphone - * @returns {Promise} Whether muting/unmuting was successful + * @param muted - Whether to mute the microphone + * @returns Whether muting/unmuting was successful */ public async setMicrophoneMuted(muted: boolean): Promise { // hasAudioDevice can block indefinitely if the window has lost focus, @@ -575,8 +590,8 @@ export class GroupCall extends TypedEventEmitter< /** * Sets the mute state of the local participants's video. - * @param {boolean} muted Whether to mute the video - * @returns {Promise} Whether muting/unmuting was successful + * @param muted - Whether to mute the video + * @returns Whether muting/unmuting was successful */ public async setLocalVideoMuted(muted: boolean): Promise { // hasAudioDevice can block indefinitely if the window has lost focus, @@ -733,8 +748,8 @@ export class GroupCall extends TypedEventEmitter< /** * Determines whether a given participant expects us to call them (versus * them calling us). - * @param userId The participant's user ID. - * @param deviceId The participant's device ID. + * @param userId - The participant's user ID. + * @param deviceId - The participant's device ID. * @returns Whether we need to place an outgoing call to the participant. */ private wantsOutgoingCall(userId: string, deviceId: string): boolean { @@ -1249,9 +1264,9 @@ export class GroupCall extends TypedEventEmitter< /** * Updates the local user's member state with the devices returned by the given function. - * @param fn A function from the current devices to the new devices. If it + * @param fn - A function from the current devices to the new devices. If it * returns null, the update will be skipped. - * @param keepAlive Whether the request should outlive the window. + * @param keepAlive - Whether the request should outlive the window. */ private async updateDevices( fn: (devices: IGroupCallRoomMemberDevice[]) => IGroupCallRoomMemberDevice[] | null, diff --git a/src/webrtc/mediaHandler.ts b/src/webrtc/mediaHandler.ts index c7c84876bf9..46943ae386a 100644 --- a/src/webrtc/mediaHandler.ts +++ b/src/webrtc/mediaHandler.ts @@ -67,7 +67,7 @@ export class MediaHandler extends TypedEventEmitter< /** * Set an audio input device to use for MatrixCalls - * @param {string} deviceId the identifier for the device + * @param deviceId - the identifier for the device * undefined treated as unset */ public async setAudioInput(deviceId: string): Promise { @@ -81,7 +81,7 @@ export class MediaHandler extends TypedEventEmitter< /** * Set audio settings for MatrixCalls - * @param {AudioSettings} opts audio options to set + * @param opts - audio options to set */ public async setAudioSettings(opts: AudioSettings): Promise { logger.info("Setting audio settings to", opts); @@ -92,7 +92,7 @@ export class MediaHandler extends TypedEventEmitter< /** * Set a video input device to use for MatrixCalls - * @param {string} deviceId the identifier for the device + * @param deviceId - the identifier for the device * undefined treated as unset */ public async setVideoInput(deviceId: string): Promise { @@ -106,8 +106,8 @@ export class MediaHandler extends TypedEventEmitter< /** * Set media input devices to use for MatrixCalls - * @param {string} audioInput the identifier for the audio device - * @param {string} videoInput the identifier for the video device + * @param audioInput - the identifier for the audio device + * @param videoInput - the identifier for the video device * undefined treated as unset */ public async setMediaInputs(audioInput: string, videoInput: string): Promise { @@ -191,10 +191,10 @@ export class MediaHandler extends TypedEventEmitter< } /** - * @param audio should have an audio track - * @param video should have a video track - * @param reusable is allowed to be reused by the MediaHandler - * @returns {MediaStream} based on passed parameters + * @param audio - should have an audio track + * @param video - should have a video track + * @param reusable - is allowed to be reused by the MediaHandler + * @returns based on passed parameters */ public async getUserMediaStream(audio: boolean, video: boolean, reusable = true): Promise { const shouldRequestAudio = audio && await this.hasAudioDevice(); @@ -296,9 +296,9 @@ export class MediaHandler extends TypedEventEmitter< } /** - * @param desktopCapturerSourceId sourceId for Electron DesktopCapturer - * @param reusable is allowed to be reused by the MediaHandler - * @returns {MediaStream} based on passed parameters + * @param desktopCapturerSourceId - sourceId for Electron DesktopCapturer + * @param reusable - is allowed to be reused by the MediaHandler + * @returns based on passed parameters */ public async getScreensharingStream(opts: IScreensharingOpts = {}, reusable = true): Promise { let stream: MediaStream; diff --git a/yarn.lock b/yarn.lock index b96adf0ef9a..9a2f147943c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1093,6 +1093,15 @@ uuid "8.3.2" xml "1.0.1" +"@es-joy/jsdoccomment@~0.36.1": + version "0.36.1" + resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz#c37db40da36e4b848da5fd427a74bae3b004a30f" + integrity sha512-922xqFsTpHs6D0BUiG4toiyPOMc8/jafnWKxz1KWgS4XzKPy2qXf1Pe6UFuNSCQqt6tOuhAWXBNuuyUhJmw9Vg== + dependencies: + comment-parser "1.3.1" + esquery "^1.4.0" + jsdoc-type-pratt-parser "~3.1.0" + "@eslint-community/eslint-utils@^4.1.0": version "4.1.2" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.1.2.tgz#14ca568ddaa291dd19a4a54498badc18c6cfab78" @@ -1427,6 +1436,21 @@ version "3.2.14" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984" +"@microsoft/tsdoc-config@0.16.2": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz#b786bb4ead00d54f53839a458ce626c8548d3adf" + integrity sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw== + dependencies: + "@microsoft/tsdoc" "0.14.2" + ajv "~6.12.6" + jju "~1.4.0" + resolve "~1.19.0" + +"@microsoft/tsdoc@0.14.2": + version "0.14.2" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz#c3ec604a0b54b9a9b87e9735dfc59e1a5da6a5fb" + integrity sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug== + "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": version "2.1.8-no-fsevents.3" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" @@ -1781,7 +1805,7 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.6.0": +"@typescript-eslint/eslint-plugin@^5.45.0": version "5.45.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.0.tgz#ffa505cf961d4844d38cfa19dcec4973a6039e41" integrity sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA== @@ -1796,7 +1820,7 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.6.0": +"@typescript-eslint/parser@^5.45.0": version "5.45.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.45.0.tgz#b18a5f6b3cf1c2b3e399e9d2df4be40d6b0ddd0e" integrity sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ== @@ -1948,7 +1972,7 @@ agent-base@6: dependencies: debug "4" -ajv@^6.10.0, ajv@^6.12.4: +ajv@^6.10.0, ajv@^6.12.4, ajv@~6.12.6: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2757,6 +2781,11 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +comment-parser@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.3.1.tgz#3d7ea3adaf9345594aedee6563f422348f165c1b" + integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -3338,11 +3367,32 @@ eslint-plugin-import@^2.26.0: resolve "^1.22.0" tsconfig-paths "^3.14.1" +eslint-plugin-jsdoc@^39.6.4: + version "39.6.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.6.4.tgz#b940aebd3eea26884a0d341785d2dc3aba6a38a7" + integrity sha512-fskvdLCfwmPjHb6e+xNGDtGgbF8X7cDwMtVLAP2WwSf9Htrx68OAx31BESBM1FAwsN2HTQyYQq7m4aW4Q4Nlag== + dependencies: + "@es-joy/jsdoccomment" "~0.36.1" + comment-parser "1.3.1" + debug "^4.3.4" + escape-string-regexp "^4.0.0" + esquery "^1.4.0" + semver "^7.3.8" + spdx-expression-parse "^3.0.1" + eslint-plugin-matrix-org@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.8.0.tgz#daa1396900a8cb1c1d88f1a370e45fc32482cd9e" integrity sha512-/Poz/F8lXYDsmQa29iPSt+kO+Jn7ArvRdq10g0CCk8wbRS0sb2zb6fvd9xL1BgR5UDQL771V0l8X32etvY5yKA== +eslint-plugin-tsdoc@^0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.17.tgz#27789495bbd8778abbf92db1707fec2ed3dfe281" + integrity sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA== + dependencies: + "@microsoft/tsdoc" "0.14.2" + "@microsoft/tsdoc-config" "0.16.2" + eslint-plugin-unicorn@^45.0.0: version "45.0.1" resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-45.0.1.tgz#2307f4620502fd955c819733ce1276bed705b736" @@ -4156,7 +4206,7 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.10.0, is-core-module@^2.8.1, is-core-module@^2.9.0: +is-core-module@^2.1.0, is-core-module@^2.10.0, is-core-module@^2.8.1, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== @@ -4809,6 +4859,11 @@ jest@^29.0.0: import-local "^3.0.2" jest-cli "^29.3.1" +jju@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" + integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA== + js-sdsl@^4.1.4: version "4.1.5" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.5.tgz#1ff1645e6b4d1b028cd3f862db88c9d887f26e2a" @@ -4839,6 +4894,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsdoc-type-pratt-parser@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.1.0.tgz#a4a56bdc6e82e5865ffd9febc5b1a227ff28e67e" + integrity sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw== + jsdom@^20.0.0: version "20.0.2" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.2.tgz#65ccbed81d5e877c433f353c58bb91ff374127db" @@ -5568,7 +5628,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.7: +path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -6119,6 +6179,14 @@ resolve@^1.1.4, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.17. path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@~1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + dependencies: + is-core-module "^2.1.0" + path-parse "^1.0.6" + retry@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -6344,7 +6412,7 @@ spdx-exceptions@^2.1.0: resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== -spdx-expression-parse@^3.0.0: +spdx-expression-parse@^3.0.0, spdx-expression-parse@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== From 16d791b0386203db9d9650768548de97f8e81245 Mon Sep 17 00:00:00 2001 From: Germain Date: Thu, 8 Dec 2022 09:54:10 +0000 Subject: [PATCH 31/60] Cache read receipts for unknown threads (#2953) --- spec/integ/matrix-client-syncing.spec.ts | 53 +++++++++++++++++++++++- src/models/room.ts | 36 ++++++++++++---- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index 5ddcb0aa01c..94e2361d7fe 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -33,8 +33,9 @@ import { IJoinedRoom, IStateEvent, IMinimalEvent, - NotificationCountType, IEphemeral, + NotificationCountType, IEphemeral, Room, } from "../../src"; +import { ReceiptType } from '../../src/@types/read_receipts'; import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync'; import * as utils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; @@ -1312,7 +1313,8 @@ describe("MatrixClient syncing", () => { join: { [roomOne]: { ephemeral: { - events: [], + events: [ + ], } as IEphemeral, timeline: { events: [ @@ -1397,6 +1399,9 @@ describe("MatrixClient syncing", () => { rooms: { join: { [roomOne]: { + ephemeral: { + events: [], + }, timeline: { events: [ utils.mkMessage({ @@ -1455,6 +1460,50 @@ describe("MatrixClient syncing", () => { expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2); }); }); + + it("caches unknown threads receipts and replay them when the thread is created", async () => { + const THREAD_ID = "$unknownthread:localhost"; + + const receipt = { + type: "m.receipt", + room_id: "!foo:bar", + content: { + "$event1:localhost": { + [ReceiptType.Read]: { + "@alice:localhost": { ts: 666, thread_id: THREAD_ID }, + }, + }, + }, + }; + syncData.rooms.join[roomOne].ephemeral.events = [receipt]; + + httpBackend!.when("GET", "/sync").respond(200, syncData); + client!.startClient(); + + return Promise.all([ + httpBackend!.flushAllExpected(), + awaitSyncEvent(), + ]).then(() => { + const room = client?.getRoom(roomOne); + expect(room).toBeInstanceOf(Room); + + expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(true); + + const thread = room!.createThread(THREAD_ID, undefined, [], true); + + expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(false); + + const receipt = thread.getReadReceiptForUserId("@alice:localhost"); + + expect(receipt).toStrictEqual({ + "data": { + "thread_id": "$unknownthread:localhost", + "ts": 666, + }, + "eventId": "$event1:localhost", + }); + }); + }); }); describe("of a room", () => { diff --git a/src/models/room.ts b/src/models/room.ts index 48de9a58c1b..95c33614c74 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -311,6 +311,7 @@ export class Room extends ReadReceipt { private txnToEvent: Record = {}; // Pending in-flight requests { string: MatrixEvent } private notificationCounts: NotificationCount = {}; private readonly threadNotifications = new Map(); + public readonly cachedThreadReadReceipts = new Map(); private readonly timelineSets: EventTimelineSet[]; public readonly threadsTimelineSets: EventTimelineSet[] = []; // any filtered timeline sets we're maintaining for this room @@ -2146,6 +2147,16 @@ export class Room extends ReadReceipt { this.updateThreadRootEvents(thread, toStartOfTimeline, false); } + // Pulling all the cached thread read receipts we've discovered when we + // did an initial sync, and applying them to the thread now that it exists + // on the client side + if (this.cachedThreadReadReceipts.has(threadId)) { + for (const { event, synthetic } of this.cachedThreadReadReceipts.get(threadId)!) { + this.addReceipt(event, synthetic); + } + this.cachedThreadReadReceipts.delete(threadId); + } + this.emit(ThreadEvent.New, thread, toStartOfTimeline); return thread; @@ -2727,13 +2738,24 @@ export class Room extends ReadReceipt { const receiptDestination: Thread | this | undefined = receiptForMainTimeline ? this : this.threads.get(receipt.thread_id ?? ""); - receiptDestination?.addReceiptToStructure( - eventId, - receiptType as ReceiptType, - userId, - receipt, - synthetic, - ); + + if (receiptDestination) { + receiptDestination.addReceiptToStructure( + eventId, + receiptType as ReceiptType, + userId, + receipt, + synthetic, + ); + } else { + // The thread does not exist locally, keep the read receipt + // in a cache locally, and re-apply the `addReceipt` logic + // when the thread is created + this.cachedThreadReadReceipts.set(receipt.thread_id!, [ + ...(this.cachedThreadReadReceipts.get(receipt.thread_id!) ?? []), + { event, synthetic }, + ]); + } }); }); }); From 224e592701003eb3ba4800bbb9e136e3cb317a0c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 8 Dec 2022 10:43:20 +0000 Subject: [PATCH 32/60] Fix `examples/browser/browserTest.js` (#2952) This seems to have been broken for ages --- examples/browser/README.md | 4 ++-- examples/browser/browserTest.js | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/examples/browser/README.md b/examples/browser/README.md index 1253d80009c..45c1d20217c 100644 --- a/examples/browser/README.md +++ b/examples/browser/README.md @@ -1,9 +1,9 @@ To try it out, **you must build the SDK first** and then host this folder: ``` - $ npm run build + $ yarn build $ cd examples/browser - $ python -m SimpleHTTPServer 8003 + $ python -m http.server 8003 ``` Then visit ``http://localhost:8003``. diff --git a/examples/browser/browserTest.js b/examples/browser/browserTest.js index e6623b382b6..891dd176b06 100644 --- a/examples/browser/browserTest.js +++ b/examples/browser/browserTest.js @@ -1,11 +1,7 @@ console.log("Loading browser sdk"); -var client = matrixcs.createClient("https://matrix.org"); -client.publicRooms(function (err, data) { - if (err) { - console.error("err %s", JSON.stringify(err)); - return; - } +var client = matrixcs.createClient({baseUrl: "https://matrix.org"}); +client.publicRooms().then(function (data) { console.log("data %s [...]", JSON.stringify(data).substring(0, 100)); console.log("Congratulations! The SDK is working on the browser!"); var result = document.getElementById("result"); From 39cf212628ec008a3c9c38eac165a5ff2a8e25c7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 8 Dec 2022 11:53:38 +0000 Subject: [PATCH 33/60] Expose a new 'userHasCrossSigningKeys' method (#2950) --- spec/unit/crypto/cross-signing.spec.ts | 66 +++++++++++++++++++++++++- src/client.ts | 13 +++++ src/crypto/index.ts | 13 +++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/spec/unit/crypto/cross-signing.spec.ts b/spec/unit/crypto/cross-signing.spec.ts index fe5b37eb206..99ad5d0dafe 100644 --- a/spec/unit/crypto/cross-signing.spec.ts +++ b/spec/unit/crypto/cross-signing.spec.ts @@ -23,7 +23,7 @@ import HttpBackend from "matrix-mock-request"; import * as olmlib from "../../../src/crypto/olmlib"; import { MatrixError } from '../../../src/http-api'; import { logger } from '../../../src/logger'; -import { ICrossSigningKey, ICreateClientOpts, ISignedKey } from '../../../src/client'; +import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from '../../../src/client'; import { CryptoEvent, IBootstrapCrossSigningOpts } from '../../../src/crypto'; import { IDevice } from '../../../src/crypto/deviceinfo'; import { TestClient } from '../../TestClient'; @@ -1137,3 +1137,67 @@ describe("Cross Signing", function() { alice.stopClient(); }); }); + +describe("userHasCrossSigningKeys", function() { + if (!global.Olm) { + return; + } + + beforeAll(() => { + return global.Olm.init(); + }); + + let aliceClient: MatrixClient; + let httpBackend: HttpBackend; + beforeEach(async () => { + const testClient = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); + aliceClient = testClient.client; + httpBackend = testClient.httpBackend; + }); + + afterEach(() => { + aliceClient.stopClient(); + }); + + it("should download devices and return true if one is a cross-signing key", async () => { + httpBackend + .when("POST", "/keys/query") + .respond(200, { + "master_keys": { + "@alice:example.com": { + user_id: "@alice:example.com", + usage: ["master"], + keys: { + "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": + "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk", + }, + }, + }, + }); + + let result: boolean; + await Promise.all([ + httpBackend.flush("/keys/query"), + aliceClient.userHasCrossSigningKeys().then((res) => {result = res;}), + ]); + expect(result!).toBeTruthy(); + }); + + it("should download devices and return false if there is no cross-signing key", async () => { + httpBackend + .when("POST", "/keys/query") + .respond(200, {}); + + let result: boolean; + await Promise.all([ + httpBackend.flush("/keys/query"), + aliceClient.userHasCrossSigningKeys().then((res) => {result = res;}), + ]); + expect(result!).toBeFalsy(); + }); + + it("throws an error if crypto is disabled", () => { + aliceClient.crypto = undefined; + expect(() => aliceClient.userHasCrossSigningKeys()).toThrowError("encryption disabled"); + }); +}); diff --git a/src/client.ts b/src/client.ts index fba259a5671..ce44d32fd66 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2473,6 +2473,19 @@ export class MatrixClient extends TypedEventEmitter { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + return this.crypto.userHasCrossSigningKeys(); + } + /** * Checks whether cross signing: * - is enabled on this account and trusted by this device diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 65b43a635a0..9191522da25 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -718,6 +718,19 @@ export class Crypto extends TypedEventEmitter { + await this.downloadKeys([this.userId]); + return this.deviceList.getStoredCrossSigningForUser(this.userId) !== null; + } + /** * Checks whether cross signing: * - is enabled on this account and trusted by this device From ae849fdd46cd556e9916fadaff625b961e8c1403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Dec 2022 19:51:05 +0100 Subject: [PATCH 34/60] Minor VoIP stack improvements (#2946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add `IGroupCallRoomState` Signed-off-by: Šimon Brandner * Export values into `const`s Signed-off-by: Šimon Brandner * Add `should correctly emit LengthChanged` Signed-off-by: Šimon Brandner * Add `ICE disconnected timeout` Signed-off-by: Šimon Brandner * Improve typing Signed-off-by: Šimon Brandner * Don't cast `getContent()` Signed-off-by: Šimon Brandner * Use `Date.now()` for call length Signed-off-by: Šimon Brandner * Type fix Signed-off-by: Šimon Brandner Signed-off-by: Šimon Brandner --- spec/test-utils/webrtc.ts | 4 +++ spec/unit/webrtc/call.spec.ts | 46 +++++++++++++++++++++++++++++++++++ src/webrtc/call.ts | 19 +++++++++------ src/webrtc/groupCall.ts | 28 ++++++++++++++------- 4 files changed, 81 insertions(+), 16 deletions(-) diff --git a/spec/test-utils/webrtc.ts b/spec/test-utils/webrtc.ts index c2f1a0b25a3..84d1f4868f4 100644 --- a/spec/test-utils/webrtc.ts +++ b/spec/test-utils/webrtc.ts @@ -115,12 +115,14 @@ export class MockRTCPeerConnection { private negotiationNeededListener?: () => void; public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void; + public iceConnectionStateChangeListener?: () => void; public onTrackListener?: (e: RTCTrackEvent) => void; public needsNegotiation = false; public readyToNegotiate: Promise; private onReadyToNegotiate?: () => void; public localDescription: RTCSessionDescription; public signalingState: RTCSignalingState = "stable"; + public iceConnectionState: RTCIceConnectionState = "connected"; public transceivers: MockRTCRtpTransceiver[] = []; public static triggerAllNegotiations(): void { @@ -156,6 +158,8 @@ export class MockRTCPeerConnection { this.negotiationNeededListener = listener; } else if (type == 'icecandidate') { this.iceCandidateListener = listener; + } else if (type === 'iceconnectionstatechange') { + this.iceConnectionStateChangeListener = listener; } else if (type == 'track') { this.onTrackListener = listener; } diff --git a/spec/unit/webrtc/call.spec.ts b/spec/unit/webrtc/call.spec.ts index 1d4f9f03401..eb5d4c18e80 100644 --- a/spec/unit/webrtc/call.spec.ts +++ b/spec/unit/webrtc/call.spec.ts @@ -1458,4 +1458,50 @@ describe('Call', function() { expect(call.hasPeerConnection).toBe(true); }); }); + + it("should correctly emit LengthChanged", async () => { + const advanceByArray = [2, 3, 5]; + const lengthChangedListener = jest.fn(); + + jest.useFakeTimers(); + call.addListener(CallEvent.LengthChanged, lengthChangedListener); + await fakeIncomingCall(client, call, "1"); + (call.peerConn as unknown as MockRTCPeerConnection).iceConnectionStateChangeListener!(); + + let hasAdvancedBy = 0; + for (const advanceBy of advanceByArray) { + jest.advanceTimersByTime(advanceBy * 1000); + hasAdvancedBy += advanceBy; + + expect(lengthChangedListener).toHaveBeenCalledTimes(hasAdvancedBy); + expect(lengthChangedListener).toBeCalledWith(hasAdvancedBy); + } + }); + + describe("ICE disconnected timeout", () => { + let mockPeerConn: MockRTCPeerConnection; + + beforeEach(async () => { + jest.useFakeTimers(); + jest.spyOn(call, "hangup"); + + await fakeIncomingCall(client, call, "1"); + + mockPeerConn = (call.peerConn as unknown as MockRTCPeerConnection); + mockPeerConn.iceConnectionState = "disconnected"; + mockPeerConn.iceConnectionStateChangeListener!(); + }); + + it("should hang up after being disconnected for 30 seconds", () => { + jest.advanceTimersByTime(31 * 1000); + expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false); + }); + + it("should not hangup if we've managed to re-connect", () => { + mockPeerConn.iceConnectionState = "connected"; + mockPeerConn.iceConnectionStateChangeListener!(); + jest.advanceTimersByTime(31 * 1000); + expect(call.hangup).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index c9bcac1124c..0d1af81a369 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -253,7 +253,11 @@ const VOIP_PROTO_VERSION = "1"; const FALLBACK_ICE_SERVER = 'stun:turn.matrix.org'; /** The length of time a call can be ringing for. */ -const CALL_TIMEOUT_MS = 60000; +const CALL_TIMEOUT_MS = 60 * 1000; // ms +/** The time after which we increment callLength */ +const CALL_LENGTH_INTERVAL = 1000; // ms +/** The time after which we end the call, if ICE got disconnected */ +const ICE_DISCONNECTED_TIMEOUT = 30 * 1000; // ms export class CallError extends Error { public readonly code: string; @@ -376,7 +380,7 @@ export class MatrixCall extends TypedEventEmitter; - private callLength = 0; + private callStartTime?: number; private opponentDeviceId?: string; private opponentDeviceInfo?: DeviceInfo; @@ -2083,11 +2087,12 @@ export class MatrixCall extends TypedEventEmitter { - this.callLength++; - this.emit(CallEvent.LengthChanged, this.callLength); - }, 1000); + this.emit(CallEvent.LengthChanged, Math.round((Date.now() - this.callStartTime!) / 1000)); + }, CALL_LENGTH_INTERVAL); } } else if (this.peerConn?.iceConnectionState == 'failed') { // Firefox for Android does not yet have support for restartIce() @@ -2104,7 +2109,7 @@ export class MatrixCall extends TypedEventEmitter { logger.info(`Hanging up call ${this.callId} (ICE disconnected for too long)`); this.hangup(CallErrorCode.IceFailed, false); - }, 30 * 1000); + }, ICE_DISCONNECTED_TIMEOUT); this.state = CallState.Connecting; } diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index 51e1a0a7835..58281b0e9eb 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -121,9 +121,17 @@ export interface IGroupCallDataChannelOptions { protocol: string; } +export interface IGroupCallRoomState { + "m.intent": GroupCallIntent; + "m.type": GroupCallType; + "io.element.ptt"?: boolean; + // TODO: Specify data-channels + "dataChannelsEnabled"?: boolean; + "dataChannelOptions"?: IGroupCallDataChannelOptions; +} + export interface IGroupCallRoomMemberFeed { purpose: SDPStreamMetadataPurpose; - // TODO: Sources for adaptive bitrate } export interface IGroupCallRoomMemberDevice { @@ -228,17 +236,19 @@ export class GroupCall extends TypedEventEmitter< this.client.groupCallEventHandler!.groupCalls.set(this.room.roomId, this); this.client.emit(GroupCallEventHandlerEvent.Outgoing, this); + const groupCallState: IGroupCallRoomState = { + "m.intent": this.intent, + "m.type": this.type, + "io.element.ptt": this.isPtt, + // TODO: Specify data-channels better + "dataChannelsEnabled": this.dataChannelsEnabled, + "dataChannelOptions": this.dataChannelsEnabled ? this.dataChannelOptions : undefined, + }; + await this.client.sendStateEvent( this.room.roomId, EventType.GroupCallPrefix, - { - "m.intent": this.intent, - "m.type": this.type, - "io.element.ptt": this.isPtt, - // TODO: Specify datachannels - "dataChannelsEnabled": this.dataChannelsEnabled, - "dataChannelOptions": this.dataChannelOptions, - }, + groupCallState, this.groupCallId, ); From 08a9073bd58708494d918fbba8a3e3443cdc8118 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Fri, 9 Dec 2022 09:34:01 +0100 Subject: [PATCH 35/60] Add prettier --- .eslintrc.js | 2 -- .prettierignore | 20 ++++++++++++++++++++ .prettierrc.js | 1 + package.json | 8 +++++--- yarn.lock | 15 ++++++++++++--- 5 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.js diff --git a/.eslintrc.js b/.eslintrc.js index c1cc4dc4caa..c73e8a44d3f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,8 +36,6 @@ module.exports = { }], "arrow-parens": "off", "prefer-promise-reject-errors": "off", - "quotes": "off", - "indent": "off", "no-constant-condition": "off", "no-async-promise-executor": "off", // We use a `logger` intermediary module diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..4e86facef78 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,20 @@ +/_docs +.DS_Store + +/.npmrc +/*.log +package-lock.json +.lock-wscript +build/Release +coverage +lib-cov +out +/dist +/lib + +# version file and tarball created by `npm pack` / `yarn pack` +/git-revision.txt +/matrix-js-sdk-*.tgz + +.vscode +.vscode/ diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000000..6a17910f1a0 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require("eslint-plugin-matrix-org/.prettierrc.js"); diff --git a/package.json b/package.json index 2685e5d2830..331033b47de 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,8 @@ "build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js", "gendoc": "typedoc", "lint": "yarn lint:types && yarn lint:js", - "lint:js": "eslint --max-warnings 0 src spec", - "lint:js-fix": "eslint --fix src spec", + "lint:js": "eslint --max-warnings 0 src spec && prettier --check .", + "lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src spec", "lint:types": "tsc --noEmit", "test": "jest", "test:watch": "jest --watch", @@ -99,10 +99,11 @@ "domexception": "^4.0.0", "eslint": "8.28.0", "eslint-config-google": "^0.14.0", + "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-typescript": "^3.5.1", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^39.6.4", - "eslint-plugin-matrix-org": "^0.8.0", + "eslint-plugin-matrix-org": "https://github.com/matrix-org/eslint-plugin-matrix-org.git#weeman1337/prettier", "eslint-plugin-tsdoc": "^0.2.17", "eslint-plugin-unicorn": "^45.0.0", "exorcist": "^2.0.0", @@ -112,6 +113,7 @@ "jest-localstorage-mock": "^2.4.6", "jest-mock": "^29.0.0", "matrix-mock-request": "^2.5.0", + "prettier": "2.8.0", "rimraf": "^3.0.2", "terser": "^5.5.1", "tsify": "^5.0.2", diff --git a/yarn.lock b/yarn.lock index 9a2f147943c..d562f256aee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3320,6 +3320,11 @@ eslint-config-google@^0.14.0: resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a" integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== +eslint-config-prettier@^8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== + eslint-import-resolver-node@^0.3.6: version "0.3.6" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" @@ -3380,10 +3385,9 @@ eslint-plugin-jsdoc@^39.6.4: semver "^7.3.8" spdx-expression-parse "^3.0.1" -eslint-plugin-matrix-org@^0.8.0: +"eslint-plugin-matrix-org@https://github.com/matrix-org/eslint-plugin-matrix-org.git#weeman1337/prettier": version "0.8.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.8.0.tgz#daa1396900a8cb1c1d88f1a370e45fc32482cd9e" - integrity sha512-/Poz/F8lXYDsmQa29iPSt+kO+Jn7ArvRdq10g0CCk8wbRS0sb2zb6fvd9xL1BgR5UDQL771V0l8X32etvY5yKA== + resolved "https://github.com/matrix-org/eslint-plugin-matrix-org.git#4c5e82dc6daa70b5d71ca8f8a55ff2e2cb6f802e" eslint-plugin-tsdoc@^0.2.17: version "0.2.17" @@ -5703,6 +5707,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== +prettier@2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.0.tgz#c7df58393c9ba77d6fba3921ae01faf994fb9dc9" + integrity sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA== + pretty-format@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.3.tgz#c9fba8cedf99ce50963a11b27d982a9ae90970d5" From 349c2c2587c2885bb69eda4aa078b5383724cf5e Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Fri, 9 Dec 2022 09:38:20 +0100 Subject: [PATCH 36/60] Apply prettier formatting --- .babelrc | 15 +- .eslintrc.js | 156 +- .github/PULL_REQUEST_TEMPLATE.md | 6 +- .github/renovate.json | 6 +- .github/workflows/backport.yml | 52 +- .github/workflows/docs-pr-netlify.yaml | 56 +- .github/workflows/notify-downstream.yaml | 44 +- .github/workflows/pull_request.yaml | 164 +- .github/workflows/release-npm.yml | 66 +- .github/workflows/release.yml | 108 +- .github/workflows/sonarcloud.yml | 88 +- .github/workflows/sonarqube.yml | 74 +- .github/workflows/static_analysis.yml | 112 +- .github/workflows/tests.yml | 100 +- .github/workflows/upgrade_dependencies.yml | 64 +- CHANGELOG.md | 8015 +++++++++-------- CONTRIBUTING.md | 4 +- README.md | 295 +- docs/storage-notes.md | 54 +- examples/browser/README.md | 2 +- examples/browser/browserTest.js | 2 +- examples/browser/index.html | 31 +- .../olm-device-export-import.html | 99 +- .../olm-device-export-import.js | 115 +- examples/node/README.md | 3 +- examples/node/app.js | 292 +- examples/node/package.json | 24 +- examples/voip/README.md | 2 +- examples/voip/browserTest.js | 49 +- examples/voip/index.html | 40 +- package.json | 284 +- scripts/switch_package_to_release.js | 12 +- spec/TestClient.ts | 86 +- spec/browserify/sync-browserify.spec.ts | 8 +- spec/integ/devicelist-integ.spec.ts | 472 +- spec/integ/matrix-client-crypto.spec.ts | 175 +- .../integ/matrix-client-event-emitter.spec.ts | 170 +- .../matrix-client-event-timeline.spec.ts | 1092 +-- spec/integ/matrix-client-methods.spec.ts | 1395 +-- spec/integ/matrix-client-opts.spec.ts | 118 +- spec/integ/matrix-client-relations.spec.ts | 75 +- spec/integ/matrix-client-retrying.spec.ts | 108 +- .../integ/matrix-client-room-timeline.spec.ts | 600 +- spec/integ/matrix-client-syncing.spec.ts | 1163 +-- spec/integ/megolm-backup.spec.ts | 127 +- spec/integ/megolm-integ.spec.ts | 752 +- spec/integ/sliding-sync-sdk.spec.ts | 207 +- spec/integ/sliding-sync.spec.ts | 1340 +-- spec/olm-loader.ts | 6 +- spec/slowReporter.js | 6 +- spec/test-utils/beacon.ts | 33 +- spec/test-utils/client.ts | 5 +- spec/test-utils/flushPromises.ts | 2 +- spec/test-utils/test-utils.ts | 42 +- spec/test-utils/thread.ts | 68 +- spec/test-utils/webrtc.ts | 211 +- spec/unit/ReEmitter.spec.ts | 16 +- spec/unit/autodiscovery.spec.ts | 697 +- spec/unit/content-helpers.spec.ts | 208 +- spec/unit/content-repo.spec.ts | 72 +- spec/unit/crypto.spec.ts | 543 +- spec/unit/crypto/CrossSigningInfo.spec.ts | 224 +- spec/unit/crypto/DeviceList.spec.ts | 170 +- spec/unit/crypto/algorithms/megolm.spec.ts | 737 +- spec/unit/crypto/algorithms/olm.spec.ts | 116 +- spec/unit/crypto/backup.spec.ts | 420 +- spec/unit/crypto/cross-signing.spec.ts | 402 +- spec/unit/crypto/crypto-utils.ts | 18 +- spec/unit/crypto/dehydration.spec.ts | 55 +- .../crypto/outgoing-room-key-requests.spec.ts | 72 +- spec/unit/crypto/secrets.spec.ts | 300 +- .../crypto/verification/InRoomChannel.spec.ts | 41 +- spec/unit/crypto/verification/qr_code.spec.ts | 6 +- spec/unit/crypto/verification/request.spec.ts | 14 +- spec/unit/crypto/verification/sas.spec.ts | 203 +- .../verification/secret_request.spec.ts | 41 +- .../setDeviceVerification.spec.ts | 10 +- spec/unit/crypto/verification/util.ts | 73 +- .../verification/verification_request.spec.ts | 131 +- spec/unit/embedded.spec.ts | 40 +- spec/unit/event-mapper.spec.ts | 6 +- spec/unit/event-timeline-set.spec.ts | 146 +- spec/unit/event-timeline.spec.ts | 377 +- spec/unit/filter-component.spec.ts | 106 +- spec/unit/filter.spec.ts | 22 +- spec/unit/http-api/fetch.spec.ts | 23 +- spec/unit/http-api/index.spec.ts | 16 +- spec/unit/http-api/utils.spec.ts | 188 +- spec/unit/interactive-auth.spec.ts | 153 +- spec/unit/local_notifications.spec.ts | 6 +- spec/unit/location.spec.ts | 33 +- spec/unit/login.spec.ts | 38 +- spec/unit/matrix-client.spec.ts | 546 +- spec/unit/models/MSC3089Branch.spec.ts | 162 +- spec/unit/models/MSC3089TreeSpace.spec.ts | 377 +- spec/unit/models/beacon.spec.ts | 255 +- spec/unit/models/event.spec.ts | 25 +- spec/unit/models/thread.spec.ts | 20 +- spec/unit/notifications.spec.ts | 21 +- spec/unit/pusher.spec.ts | 6 +- spec/unit/pushprocessor.spec.ts | 373 +- spec/unit/queueToDevice.spec.ts | 156 +- spec/unit/read-receipt.spec.ts | 128 +- spec/unit/realtime-callbacks.spec.ts | 36 +- spec/unit/relations.spec.ts | 98 +- spec/unit/rendezvous/DummyTransport.ts | 14 +- spec/unit/rendezvous/ecdh.spec.ts | 52 +- spec/unit/rendezvous/rendezvous.spec.ts | 170 +- .../rendezvous/simpleHttpTransport.spec.ts | 131 +- spec/unit/room-member.spec.ts | 291 +- spec/unit/room-state.spec.ts | 803 +- spec/unit/room.spec.ts | 2167 +++-- spec/unit/scheduler.spec.ts | 370 +- spec/unit/stores/indexeddb.spec.ts | 9 +- spec/unit/sync-accumulator.spec.ts | 329 +- spec/unit/timeline-window.spec.ts | 150 +- spec/unit/user.spec.ts | 34 +- spec/unit/utils.spec.ts | 313 +- spec/unit/webrtc/call.spec.ts | 751 +- spec/unit/webrtc/callEventHandler.spec.ts | 99 +- spec/unit/webrtc/groupCall.spec.ts | 414 +- .../unit/webrtc/groupCallEventHandler.spec.ts | 78 +- spec/unit/webrtc/mediaHandler.spec.ts | 120 +- src/@types/PushRules.ts | 29 +- src/@types/beacon.ts | 9 +- src/@types/event.ts | 28 +- src/@types/global.d.ts | 28 +- src/@types/location.ts | 11 +- src/@types/partials.ts | 3 +- src/@types/read_receipts.ts | 2 +- src/@types/sync.ts | 3 +- src/@types/uia.ts | 2 +- src/NamespacedValue.ts | 3 +- src/ReEmitter.ts | 7 +- src/ToDeviceMessageQueue.ts | 8 +- src/autodiscovery.ts | 39 +- src/client.ts | 1850 ++-- src/content-helpers.ts | 40 +- src/content-repo.ts | 6 +- src/crypto/CrossSigning.ts | 154 +- src/crypto/DeviceList.ts | 271 +- src/crypto/EncryptionSetup.ts | 47 +- src/crypto/OlmDevice.ts | 691 +- src/crypto/OutgoingRoomKeyRequestManager.ts | 180 +- src/crypto/RoomList.ts | 24 +- src/crypto/SecretStorage.ts | 87 +- src/crypto/aes.ts | 41 +- src/crypto/algorithms/base.ts | 20 +- src/crypto/algorithms/megolm.ts | 531 +- src/crypto/algorithms/olm.ts | 144 +- src/crypto/backup.ts | 158 +- src/crypto/dehydration.ts | 104 +- src/crypto/deviceinfo.ts | 4 +- src/crypto/index.ts | 851 +- src/crypto/key_passphrase.ts | 24 +- src/crypto/olmlib.ts | 116 +- src/crypto/recoverykey.ts | 18 +- src/crypto/store/base.ts | 13 +- .../store/indexeddb-crypto-store-backend.ts | 229 +- src/crypto/store/indexeddb-crypto-store.ts | 141 +- src/crypto/store/localStorage-crypto-store.ts | 89 +- src/crypto/store/memory-crypto-store.ts | 58 +- src/crypto/verification/Base.ts | 55 +- src/crypto/verification/Error.ts | 18 +- src/crypto/verification/QRCode.ts | 13 +- src/crypto/verification/SAS.ts | 259 +- src/crypto/verification/SASDecimal.ts | 6 +- .../verification/request/InRoomChannel.ts | 31 +- .../verification/request/ToDeviceChannel.ts | 30 +- .../request/VerificationRequest.ts | 105 +- src/embedded.ts | 68 +- src/errors.ts | 8 +- src/event-mapper.ts | 13 +- src/feature.ts | 9 +- src/filter-component.ts | 55 +- src/filter.ts | 22 +- src/http-api/fetch.ts | 24 +- src/http-api/index.ts | 32 +- src/index.ts | 1 - src/indexeddb-worker.ts | 1 - src/interactive-auth.ts | 25 +- src/logger.ts | 12 +- src/matrix.ts | 29 +- src/models/MSC3089Branch.ts | 72 +- src/models/MSC3089TreeSpace.ts | 121 +- src/models/beacon.ts | 42 +- src/models/event-timeline-set.ts | 124 +- src/models/event-timeline.ts | 44 +- src/models/event.ts | 132 +- src/models/invites-ignorer.ts | 53 +- src/models/read-receipt.ts | 39 +- src/models/related-relations.ts | 6 +- src/models/relations-container.ts | 20 +- src/models/relations.ts | 4 +- src/models/room-member.ts | 17 +- src/models/room-state.ts | 57 +- src/models/room-summary.ts | 1 - src/models/room.ts | 628 +- src/models/search-result.ts | 6 +- src/models/thread.ts | 122 +- src/models/typed-event-emitter.ts | 25 +- src/models/user.ts | 12 +- src/pushprocessor.ts | 91 +- src/realtime-callbacks.ts | 15 +- src/rendezvous/MSC3906Rendezvous.ts | 54 +- src/rendezvous/RendezvousChannel.ts | 6 +- src/rendezvous/RendezvousFailureReason.ts | 22 +- src/rendezvous/RendezvousIntent.ts | 4 +- src/rendezvous/RendezvousTransport.ts | 64 +- .../MSC3903ECDHv1RendezvousChannel.ts | 54 +- src/rendezvous/channels/index.ts | 3 +- src/rendezvous/index.ts | 14 +- .../MSC3886SimpleHttpRendezvousTransport.ts | 50 +- src/rendezvous/transports/index.ts | 2 +- src/room-hierarchy.ts | 17 +- src/scheduler.ts | 73 +- src/service-types.ts | 4 +- src/sliding-sync-sdk.ts | 195 +- src/sliding-sync.ts | 83 +- src/store/index.ts | 18 +- src/store/indexeddb-local-backend.ts | 92 +- src/store/indexeddb-remote-backend.ts | 44 +- src/store/indexeddb-store-worker.ts | 77 +- src/store/indexeddb.ts | 52 +- src/store/local-storage-events-emitter.ts | 12 +- src/store/memory.ts | 7 +- src/sync-accumulator.ts | 79 +- src/sync.ts | 534 +- src/timeline-window.ts | 78 +- src/utils.ts | 85 +- src/webrtc/call.ts | 518 +- src/webrtc/callEventHandler.ts | 76 +- src/webrtc/callEventTypes.ts | 4 +- src/webrtc/callFeed.ts | 6 +- src/webrtc/groupCall.ts | 308 +- src/webrtc/groupCallEventHandler.ts | 33 +- src/webrtc/mediaHandler.ts | 57 +- tsconfig-build.json | 22 +- tsconfig.json | 37 +- 239 files changed, 22143 insertions(+), 22067 deletions(-) diff --git a/.babelrc b/.babelrc index 6f4f54a93f1..6e6720bbf11 100644 --- a/.babelrc +++ b/.babelrc @@ -1,12 +1,15 @@ { "sourceMaps": true, "presets": [ - ["@babel/preset-env", { - "targets": { - "node": 10 - }, - "modules": "commonjs" - }], + [ + "@babel/preset-env", + { + "targets": { + "node": 10 + }, + "modules": "commonjs" + } + ], "@babel/preset-typescript" ], "plugins": [ diff --git a/.eslintrc.js b/.eslintrc.js index c73e8a44d3f..fd77e6c5e42 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,13 +1,6 @@ module.exports = { - plugins: [ - "matrix-org", - "import", - "jsdoc", - ], - extends: [ - "plugin:matrix-org/babel", - "plugin:import/typescript", - ], + plugins: ["matrix-org", "import", "jsdoc"], + extends: ["plugin:matrix-org/babel", "plugin:import/typescript"], env: { browser: true, node: true, @@ -28,12 +21,15 @@ module.exports = { "padded-blocks": ["error"], "no-extend-native": ["error"], "camelcase": ["error"], - "no-multi-spaces": ["error", { "ignoreEOLComments": true }], - "space-before-function-paren": ["error", { - "anonymous": "never", - "named": "never", - "asyncArrow": "always", - }], + "no-multi-spaces": ["error", { ignoreEOLComments: true }], + "space-before-function-paren": [ + "error", + { + anonymous: "never", + named: "never", + asyncArrow: "always", + }, + ], "arrow-parens": "off", "prefer-promise-reject-errors": "off", "no-constant-condition": "off", @@ -42,76 +38,78 @@ module.exports = { "no-console": "error", // restrict EventEmitters to force callers to use TypedEventEmitter - "no-restricted-imports": ["error", { - name: "events", - message: "Please use TypedEventEmitter instead", - }], - - "import/no-restricted-paths": ["error", { - "zones": [{ - "target": "./src/", - "from": "./src/index.ts", - "message": "The package index is dynamic between src and lib depending on " + - "whether release or development, target the specific module or matrix.ts instead", - }], - }], - }, - overrides: [{ - files: [ - "**/*.ts", - ], - plugins: [ - "eslint-plugin-tsdoc", + "no-restricted-imports": [ + "error", + { + name: "events", + message: "Please use TypedEventEmitter instead", + }, ], - extends: [ - "plugin:matrix-org/typescript", - ], - rules: { - // TypeScript has its own version of this - "@babel/no-invalid-this": "off", - // We're okay being explicit at the moment - "@typescript-eslint/no-empty-interface": "off", - // We disable this while we're transitioning - "@typescript-eslint/no-explicit-any": "off", - // We'd rather not do this but we do - "@typescript-eslint/ban-ts-comment": "off", - // We're okay with assertion errors when we ask for them - "@typescript-eslint/no-non-null-assertion": "off", + "import/no-restricted-paths": [ + "error", + { + zones: [ + { + target: "./src/", + from: "./src/index.ts", + message: + "The package index is dynamic between src and lib depending on " + + "whether release or development, target the specific module or matrix.ts instead", + }, + ], + }, + ], + }, + overrides: [ + { + files: ["**/*.ts"], + plugins: ["eslint-plugin-tsdoc"], + extends: ["plugin:matrix-org/typescript"], + rules: { + // TypeScript has its own version of this + "@babel/no-invalid-this": "off", - // The non-TypeScript rule produces false positives - "func-call-spacing": "off", - "@typescript-eslint/func-call-spacing": ["error"], + // We're okay being explicit at the moment + "@typescript-eslint/no-empty-interface": "off", + // We disable this while we're transitioning + "@typescript-eslint/no-explicit-any": "off", + // We'd rather not do this but we do + "@typescript-eslint/ban-ts-comment": "off", + // We're okay with assertion errors when we ask for them + "@typescript-eslint/no-non-null-assertion": "off", - "quotes": "off", - // We use a `logger` intermediary module - "no-console": "error", + // The non-TypeScript rule produces false positives + "func-call-spacing": "off", + "@typescript-eslint/func-call-spacing": ["error"], + "quotes": "off", + // We use a `logger` intermediary module + "no-console": "error", + }, }, - }, { - // We don't need amazing docs in our spec files - files: [ - "src/**/*.ts", - ], - rules: { - "tsdoc/syntax": "error", - // We use some select jsdoc rules as the tsdoc linter has only one rule - "jsdoc/no-types": "error", - "jsdoc/empty-tags": "error", - "jsdoc/check-property-names": "error", - "jsdoc/check-values": "error", - // These need a bit more work before we can enable - // "jsdoc/check-param-names": "error", - // "jsdoc/check-indentation": "error", + { + // We don't need amazing docs in our spec files + files: ["src/**/*.ts"], + rules: { + "tsdoc/syntax": "error", + // We use some select jsdoc rules as the tsdoc linter has only one rule + "jsdoc/no-types": "error", + "jsdoc/empty-tags": "error", + "jsdoc/check-property-names": "error", + "jsdoc/check-values": "error", + // These need a bit more work before we can enable + // "jsdoc/check-param-names": "error", + // "jsdoc/check-indentation": "error", + }, }, - }, { - files: [ - "spec/**/*.ts", - ], - rules: { - // We don't need super strict typing in test utilities - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-member-accessibility": "off", + { + files: ["spec/**/*.ts"], + rules: { + // We don't need super strict typing in test utilities + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-member-accessibility": "off", + }, }, - }], + ], }; diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c0b7939a69b..bbaaa050791 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,9 +2,9 @@ ## Checklist -* [ ] Tests written for new code (and old code if feasible) -* [ ] Linter and other CI checks pass -* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)) +- [ ] Tests written for new code (and old code if feasible) +- [ ] Linter and other CI checks pass +- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))