From 815370c5f95a922dc29e04dddbc7abe274399ca3 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 25 Nov 2022 13:21:16 +0000 Subject: [PATCH 1/5] sliding sync: add receipts extension --- spec/integ/sliding-sync-sdk.spec.ts | 91 +++++++++++++++++++++++++++++ src/sliding-sync-sdk.ts | 41 +++++++++++++ 2 files changed, 132 insertions(+) diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 3a43f9e12a3..5246e12aa4a 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -928,4 +928,95 @@ describe("SlidingSyncSdk", () => { expect(room.getMember(selfUserId)?.typing).toEqual(false); }); }); + + describe("ExtensionReceipts", () => { + let ext: Extension; + + beforeAll(async () => { + await setupClient(); + const hasSynced = sdk!.sync(); + await httpBackend!.flushAllExpected(); + await hasSynced; + ext = findExtension("receipts"); + }); + + it("gets enabled on the initial request only", () => { + expect(ext.onRequest(true)).toEqual({ + enabled: true, + }); + expect(ext.onRequest(false)).toEqual(undefined); + }); + + it("processes receipts", async () => { + const roomId = "!room:id"; + const alice = "@alice:alice"; + const lastEvent = mkOwnEvent(EventType.RoomMessage, { body: "hello" }); + mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, { + name: "Room with receipts", + required_state: [], + timeline: [ + mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""), + mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId), + mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""), + { + type: EventType.RoomMember, + state_key: alice, + content: {membership: "join"}, + sender: alice, + origin_server_ts: Date.now(), + event_id: "$alice", + }, + lastEvent, + ], + initial: true, + }); + const room = client!.getRoom(roomId)!; + expect(room).toBeDefined(); + expect(room.getReadReceiptForUserId(alice, true)).toBeNull(); + ext.onResponse({ + rooms: { + [roomId]: { + type: EventType.Receipt, + content: { + [lastEvent.event_id]: { + "m.read": { + [alice]: { + ts: 1234567, + } + }, + }, + }, + }, + }, + }); + const receipt = room.getReadReceiptForUserId(alice); + expect(receipt).toBeDefined(); + expect(receipt?.eventId).toEqual(lastEvent.event_id); + expect(receipt?.data.ts).toEqual(1234567); + expect(receipt?.data.thread_id).toBeFalsy(); + }); + + it("gracefully handles missing rooms when receiving receipts", async () => { + const roomId = "!room:id"; + const alice = "@alice:alice"; + const eventId = "$something"; + ext.onResponse({ + rooms: { + [roomId]: { + type: EventType.Receipt, + content: { + [eventId]: { + "m.read": { + [alice]: { + ts: 1234567, + } + }, + }, + }, + }, + }, + }); + // we expect it not to crash + }); + }); }); diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index f12b51c65b5..5dc10bb6fd2 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -273,6 +273,46 @@ class ExtensionTyping implements Extension { } } +class ExtensionReceipts implements Extension { + public constructor(private readonly client: MatrixClient) {} + + public name(): string { + return "receipts"; + } + + public when(): ExtensionState { + return ExtensionState.PostProcess; + } + + public onRequest(isInitial: boolean): object | undefined { + if (!isInitial) { + return undefined; + } + return { + enabled: true, + }; + } + + public onResponse(data: {rooms: Record}): void { + if (!data || !data.rooms) { + return; + } + + for (const roomId in data.rooms) { + const ephemeralEvents = mapEvents(this.client, roomId, [data.rooms[roomId]]); + const room = this.client.getRoom(roomId); + if (!room) { + logger.warn("got receipts for room but room doesn't exist on client:", roomId); + continue; + } + room.addEphemeralEvents(ephemeralEvents); + ephemeralEvents.forEach((e) => { + this.client.emit(ClientEvent.Event, e); + }); + } + } +} + /** * A copy of SyncApi such that it can be used as a drop-in replacement for sync v2. For the actual * sliding sync API, see sliding-sync.ts or the class SlidingSync. @@ -314,6 +354,7 @@ export class SlidingSyncSdk { new ExtensionToDevice(this.client), new ExtensionAccountData(this.client), new ExtensionTyping(this.client), + new ExtensionReceipts(this.client), ]; if (this.opts.crypto) { extensions.push( From 74147b9943f9371da87e5df9d5db52bd9f067249 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 25 Nov 2022 13:24:12 +0000 Subject: [PATCH 2/5] Linting --- spec/integ/sliding-sync-sdk.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 5246e12aa4a..6945404ddf1 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -961,7 +961,7 @@ describe("SlidingSyncSdk", () => { { type: EventType.RoomMember, state_key: alice, - content: {membership: "join"}, + content: { membership: "join" }, sender: alice, origin_server_ts: Date.now(), event_id: "$alice", @@ -982,7 +982,7 @@ describe("SlidingSyncSdk", () => { "m.read": { [alice]: { ts: 1234567, - } + }, }, }, }, @@ -1009,7 +1009,7 @@ describe("SlidingSyncSdk", () => { "m.read": { [alice]: { ts: 1234567, - } + }, }, }, }, From 6592b2c2051bd6413e8ffcf1882f626e65b2628d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 28 Nov 2022 10:58:38 +0000 Subject: [PATCH 3/5] sonarcloud --- spec/integ/sliding-sync-sdk.spec.ts | 59 +++++++++++++---------------- src/sliding-sync-sdk.ts | 53 +++++++++++++------------- 2 files changed, 53 insertions(+), 59 deletions(-) diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 6945404ddf1..72a7eeaa2e9 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -932,6 +932,27 @@ describe("SlidingSyncSdk", () => { describe("ExtensionReceipts", () => { let ext: Extension; + const generateReceiptResponse = ( + userId: string, roomId: string, eventId: string, recType: string, ts: number, + ) => { + return { + rooms: { + [roomId]: { + type: EventType.Receipt, + content: { + [eventId]: { + [recType]: { + [userId]: { + ts: ts, + }, + }, + }, + }, + }, + }, + }; + }; + beforeAll(async () => { await setupClient(); const hasSynced = sdk!.sync(); @@ -973,22 +994,9 @@ describe("SlidingSyncSdk", () => { const room = client!.getRoom(roomId)!; expect(room).toBeDefined(); expect(room.getReadReceiptForUserId(alice, true)).toBeNull(); - ext.onResponse({ - rooms: { - [roomId]: { - type: EventType.Receipt, - content: { - [lastEvent.event_id]: { - "m.read": { - [alice]: { - ts: 1234567, - }, - }, - }, - }, - }, - }, - }); + ext.onResponse( + generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567), + ); const receipt = room.getReadReceiptForUserId(alice); expect(receipt).toBeDefined(); expect(receipt?.eventId).toEqual(lastEvent.event_id); @@ -1000,22 +1008,9 @@ describe("SlidingSyncSdk", () => { const roomId = "!room:id"; const alice = "@alice:alice"; const eventId = "$something"; - ext.onResponse({ - rooms: { - [roomId]: { - type: EventType.Receipt, - content: { - [eventId]: { - "m.read": { - [alice]: { - ts: 1234567, - }, - }, - }, - }, - }, - }, - }); + ext.onResponse( + generateReceiptResponse(alice, roomId, eventId, "m.read", 1234567), + ); // we expect it not to crash }); }); diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 5dc10bb6fd2..ae60006678b 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -253,22 +253,15 @@ class ExtensionTyping implements Extension { }; } - public onResponse(data: {rooms: Record}): void { + public onResponse(data: {rooms: Record}): void { if (!data || !data.rooms) { return; } for (const roomId in data.rooms) { - const ephemeralEvents = mapEvents(this.client, roomId, [data.rooms[roomId]]); - const room = this.client.getRoom(roomId); - if (!room) { - logger.warn("got typing events for room but room doesn't exist on client:", roomId); - continue; - } - room.addEphemeralEvents(ephemeralEvents); - ephemeralEvents.forEach((e) => { - this.client.emit(ClientEvent.Event, e); - }); + processEphemeralEvents( + this.client, roomId, [data.rooms[roomId]], + ); } } } @@ -285,30 +278,23 @@ class ExtensionReceipts implements Extension { } public onRequest(isInitial: boolean): object | undefined { - if (!isInitial) { - return undefined; + if (isInitial) { + return { + enabled: true, + }; } - return { - enabled: true, - }; + return undefined; } - public onResponse(data: {rooms: Record}): void { + public onResponse(data: {rooms: Record}): void { if (!data || !data.rooms) { return; } for (const roomId in data.rooms) { - const ephemeralEvents = mapEvents(this.client, roomId, [data.rooms[roomId]]); - const room = this.client.getRoom(roomId); - if (!room) { - logger.warn("got receipts for room but room doesn't exist on client:", roomId); - continue; - } - room.addEphemeralEvents(ephemeralEvents); - ephemeralEvents.forEach((e) => { - this.client.emit(ClientEvent.Event, e); - }); + processEphemeralEvents( + this.client, roomId, [data.rooms[roomId]], + ); } } } @@ -970,3 +956,16 @@ function mapEvents(client: MatrixClient, roomId: string | undefined, events: obj return mapper(e); }); } + +function processEphemeralEvents(client: MatrixClient, roomId: string, ephEvents: IMinimalEvent[]) { + const ephemeralEvents = mapEvents(client, roomId, ephEvents); + const room = client.getRoom(roomId); + if (!room) { + logger.warn("got ephemeral events for room but room doesn't exist on client:", roomId); + return; + } + room.addEphemeralEvents(ephemeralEvents); + ephemeralEvents.forEach((e) => { + client.emit(ClientEvent.Event, e); + }); +} From c8c39052a7d4bc1b82b06dac9f2dc79696e4118d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 28 Nov 2022 11:09:03 +0000 Subject: [PATCH 4/5] Linting --- src/sliding-sync-sdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index ae60006678b..0c5524cbdc8 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -957,7 +957,7 @@ function mapEvents(client: MatrixClient, roomId: string | undefined, events: obj }); } -function processEphemeralEvents(client: MatrixClient, roomId: string, ephEvents: IMinimalEvent[]) { +function processEphemeralEvents(client: MatrixClient, roomId: string, ephEvents: IMinimalEvent[]): void { const ephemeralEvents = mapEvents(client, roomId, ephEvents); const room = client.getRoom(roomId); if (!room) { From 847766c114ab2e407e729338ff70d5f2a2c969bb Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 28 Nov 2022 18:13:17 +0000 Subject: [PATCH 5/5] Review comments --- src/sliding-sync-sdk.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 0c5524cbdc8..a787b4e3465 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -246,7 +246,7 @@ class ExtensionTyping implements Extension { public onRequest(isInitial: boolean): object | undefined { if (!isInitial) { - return undefined; + return undefined; // don't send a JSON object for subsequent requests, we don't need to. } return { enabled: true, @@ -283,7 +283,7 @@ class ExtensionReceipts implements Extension { enabled: true, }; } - return undefined; + return undefined; // don't send a JSON object for subsequent requests, we don't need to. } public onResponse(data: {rooms: Record}): void {