From f5d2fa9deb0b1443fa5e29289935518d52768998 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 16:33:09 +0100 Subject: [PATCH 1/6] Add new version for UPDATE_STATE --- src/interfaces/ApiVersion.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/interfaces/ApiVersion.ts b/src/interfaces/ApiVersion.ts index ab0546e..8c7fd9a 100644 --- a/src/interfaces/ApiVersion.ts +++ b/src/interfaces/ApiVersion.ts @@ -22,6 +22,7 @@ export enum MatrixApiVersion { export enum UnstableApiVersion { MSC2762 = "org.matrix.msc2762", + MSC2762_UPDATE_STATE = "org.matrix.msc2762_update_state", MSC2871 = "org.matrix.msc2871", MSC2873 = "org.matrix.msc2873", MSC2931 = "org.matrix.msc2931", @@ -41,6 +42,7 @@ export const CurrentApiVersions: ApiVersion[] = [ MatrixApiVersion.Prerelease2, //MatrixApiVersion.V010, UnstableApiVersion.MSC2762, + UnstableApiVersion.MSC2762_UPDATE_STATE, UnstableApiVersion.MSC2871, UnstableApiVersion.MSC2873, UnstableApiVersion.MSC2931, From 94b985b9745dc96c2ff746d2962319872c94f70e Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 16:34:27 +0100 Subject: [PATCH 2/6] Let the client only send `UpdateState` if the widget supports it. --- src/ClientWidgetApi.ts | 47 ++++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index 1fd0cd0..0e1cb95 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -40,7 +40,7 @@ import { ISupportedVersionsActionRequest, ISupportedVersionsActionResponseData, } from "./interfaces/SupportedVersionsAction"; -import { CurrentApiVersions } from "./interfaces/ApiVersion"; +import { ApiVersion, CurrentApiVersions, UnstableApiVersion } from "./interfaces/ApiVersion"; import { IScreenshotActionResponseData } from "./interfaces/ScreenshotAction"; import { IVisibilityActionRequestData } from "./interfaces/VisibilityAction"; import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponseData } from "./interfaces/IWidgetApiResponse"; @@ -138,6 +138,7 @@ import { IUpdateStateToWidgetRequestData } from "./interfaces/UpdateStateAction" export class ClientWidgetApi extends EventEmitter { public readonly transport: ITransport; + private cachedWidgetVersions: ApiVersion[] | null = null; // contentLoadedActionSent is used to check that only one ContentLoaded request is send. private contentLoadedActionSent = false; private allowedCapabilities = new Set(); @@ -230,6 +231,24 @@ export class ClientWidgetApi extends EventEmitter { this.transport.stop(); } + public async getWidgetVersions(): Promise { + if (Array.isArray(this.cachedWidgetVersions)) { + return Promise.resolve(this.cachedWidgetVersions); + } + + try { + const r = await this.transport.send( + WidgetApiToWidgetAction.SupportedApiVersions, + {}, + ); + this.cachedWidgetVersions = r.supported_versions; + return r.supported_versions; + } catch (e) { + console.warn("non-fatal error getting supported widget versions: ", e); + return []; + } + } + private beginCapabilities(): void { // widget has loaded - tell all the listeners that this.emit("preparing"); @@ -1007,7 +1026,7 @@ export class ClientWidgetApi extends EventEmitter { public async feedEvent(rawEvent: IRoomEvent, currentViewedRoomId: string): Promise; /** * Feeds an event to the widget. As a client you are expected to call this - * for every new event in every room to which you are joined or invited. + * for every new event (including state events) in every room to which you are joined or invited. * @param {IRoomEvent} rawEvent The event to (try to) send to the widget. * @returns {Promise} Resolves when delivered or if the widget is not * able to read the event due to permissions, rejects if the widget failed @@ -1076,6 +1095,7 @@ export class ClientWidgetApi extends EventEmitter { } private async flushRoomState(): Promise { + const useUpdateState = (await this.getWidgetVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE); try { // Only send a single action once all concurrent tasks have completed do await Promise.all([...this.pushRoomStateTasks]); @@ -1087,10 +1107,11 @@ export class ClientWidgetApi extends EventEmitter { events.push(...stateKeyMap.values()); } } - await this.transport.send( - WidgetApiToWidgetAction.UpdateState, - { state: events }, - ); + if (useUpdateState) { + await this.transport.send(WidgetApiToWidgetAction.UpdateState, { + state: events, + }); + } } finally { this.flushRoomStateTask = null; } @@ -1152,7 +1173,9 @@ export class ClientWidgetApi extends EventEmitter { widget failed to handle the update. */ public async feedStateUpdate(rawEvent: IRoomEvent): Promise { - if (rawEvent.state_key === undefined) throw new Error('Not a state event'); + const useUpdateState = (await this.getWidgetVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE); + + if (rawEvent.state_key === undefined) throw new Error("Not a state event"); if ( (rawEvent.room_id === this.viewedRoomId || this.canUseRoomTimeline(rawEvent.room_id)) && this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key) @@ -1160,10 +1183,12 @@ export class ClientWidgetApi extends EventEmitter { // Updates could race with the initial push of the room's state if (this.pushRoomStateTasks.size === 0) { // No initial push tasks are pending; safe to send immediately - await this.transport.send( - WidgetApiToWidgetAction.UpdateState, - { state: [rawEvent] }, - ); + if (useUpdateState) { + // Only send state updates when using UpdateState. Otherwise we will use SendEvent. + await this.transport.send(WidgetApiToWidgetAction.UpdateState, { + state: [rawEvent], + }); + } } else { // Lump the update in with whatever data will be sent in the // initial push later. Even if we set it to an "outdated" entry From ed611be5195a9f66cffa09af6d1bd9e69b173ed7 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 17:01:34 +0100 Subject: [PATCH 3/6] fix tests --- test/ClientWidgetApi-test.ts | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index fd446d7..3e18112 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -19,17 +19,17 @@ import { waitFor } from '@testing-library/dom'; import { ClientWidgetApi } from "../src/ClientWidgetApi"; import { WidgetDriver } from "../src/driver/WidgetDriver"; -import { UnstableApiVersion } from '../src/interfaces/ApiVersion'; -import { Capability } from '../src/interfaces/Capabilities'; -import { IRoomEvent } from '../src/interfaces/IRoomEvent'; -import { IWidgetApiRequest } from '../src/interfaces/IWidgetApiRequest'; -import { IReadRelationsFromWidgetActionRequest } from '../src/interfaces/ReadRelationsAction'; -import { ISupportedVersionsActionRequest } from '../src/interfaces/SupportedVersionsAction'; -import { IUserDirectorySearchFromWidgetActionRequest } from '../src/interfaces/UserDirectorySearchAction'; -import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from '../src/interfaces/WidgetApiAction'; -import { WidgetApiDirection } from '../src/interfaces/WidgetApiDirection'; -import { Widget } from '../src/models/Widget'; -import { PostmessageTransport } from '../src/transport/PostmessageTransport'; +import { CurrentApiVersions, UnstableApiVersion } from "../src/interfaces/ApiVersion"; +import { Capability } from "../src/interfaces/Capabilities"; +import { IRoomEvent } from "../src/interfaces/IRoomEvent"; +import { IWidgetApiRequest } from "../src/interfaces/IWidgetApiRequest"; +import { IReadRelationsFromWidgetActionRequest } from "../src/interfaces/ReadRelationsAction"; +import { ISupportedVersionsActionRequest } from "../src/interfaces/SupportedVersionsAction"; +import { IUserDirectorySearchFromWidgetActionRequest } from "../src/interfaces/UserDirectorySearchAction"; +import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "../src/interfaces/WidgetApiAction"; +import { WidgetApiDirection } from "../src/interfaces/WidgetApiDirection"; +import { Widget } from "../src/models/Widget"; +import { PostmessageTransport } from "../src/transport/PostmessageTransport"; import { IDownloadFileActionFromWidgetActionRequest, IGetOpenIDActionRequest, @@ -792,6 +792,14 @@ describe('ClientWidgetApi', () => { const roomId = '!room:example.org'; const otherRoomId = '!other-room:example.org'; clientWidgetApi.setViewedRoomId(roomId); + + jest.spyOn(transport, "send").mockImplementation((action, data) => { + if (action === WidgetApiToWidgetAction.SupportedApiVersions) { + return Promise.resolve({ supported_versions: CurrentApiVersions }); + } + return Promise.resolve({}); + }); + const topicEvent = createRoomEvent({ room_id: roomId, type: 'm.room.topic', From 4e6d4869b4caa3ceef710e285cc52af149ddc15a Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 18:14:26 +0100 Subject: [PATCH 4/6] add vscode ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ec6947a..f65f07c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ lib/ dist/ .npmrc .idea +.vscode # Logs logs From 367f47cd8b4af52fc0fb749f8e5a1340c17274b1 Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 23 Jan 2025 18:26:01 +0100 Subject: [PATCH 5/6] test for not sending update event if not available in versions --- test/ClientWidgetApi-test.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/ClientWidgetApi-test.ts b/test/ClientWidgetApi-test.ts index 3e18112..6743ad6 100644 --- a/test/ClientWidgetApi-test.ts +++ b/test/ClientWidgetApi-test.ts @@ -912,6 +912,37 @@ describe('ClientWidgetApi', () => { }); }); + describe('dont receive UpdateState if version not supported', () => { + it('syncs initial state and feeds updates', async () => { + const roomId = '!room:example.org'; + clientWidgetApi.setViewedRoomId(roomId); + jest.spyOn(transport, "send").mockImplementation((action, data) => { + if (action === WidgetApiToWidgetAction.SupportedApiVersions) { + return Promise.resolve({ supported_versions: [] }); + } + return Promise.resolve({}); + }); + + await loadIframe([ + 'org.matrix.msc2762.receive.state_event:m.room.join_rules#', + ]); + + const newJoinRulesEvent = createRoomEvent({ + room_id: roomId, + type: 'm.room.join_rules', + state_key: '', + content: { join_rule: 'invite' }, + }); + clientWidgetApi.feedStateUpdate(newJoinRulesEvent); + + await waitFor(() => { + + // Only the updated join rules should have been delivered + expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState); + }); + }); + }); + describe('update_delayed_event action', () => { it('fails to update delayed events', async () => { const event: IUpdateDelayedEventFromWidgetActionRequest = { From 8939a5c6b9c3690d87b76221724cf610c50bbd30 Mon Sep 17 00:00:00 2001 From: Timo Date: Fri, 24 Jan 2025 09:30:17 +0100 Subject: [PATCH 6/6] review --- src/ClientWidgetApi.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ClientWidgetApi.ts b/src/ClientWidgetApi.ts index 0e1cb95..4449d10 100644 --- a/src/ClientWidgetApi.ts +++ b/src/ClientWidgetApi.ts @@ -304,7 +304,7 @@ export class ClientWidgetApi extends EventEmitter { private onIframeLoad(ev: Event): void { if (this.widget.waitForIframeLoad) { - // If the widget is set to waitForIframeLoad the capabilities immediatly get setup after load. + // If the widget is set to waitForIframeLoad the capabilities immediately get setup after load. // The client does not wait for the ContentLoaded action. this.beginCapabilities(); } else { @@ -1095,7 +1095,6 @@ export class ClientWidgetApi extends EventEmitter { } private async flushRoomState(): Promise { - const useUpdateState = (await this.getWidgetVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE); try { // Only send a single action once all concurrent tasks have completed do await Promise.all([...this.pushRoomStateTasks]); @@ -1107,7 +1106,8 @@ export class ClientWidgetApi extends EventEmitter { events.push(...stateKeyMap.values()); } } - if (useUpdateState) { + if ((await this.getWidgetVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE)) { + // Only send state updates when using UpdateState. Otherwise the SendEvent action will be responsible for state updates. await this.transport.send(WidgetApiToWidgetAction.UpdateState, { state: events, }); @@ -1170,11 +1170,9 @@ export class ClientWidgetApi extends EventEmitter { * room state entry. * @returns {Promise} Resolves when delivered or if the widget is not * able to receive the room state due to permissions, rejects if the - widget failed to handle the update. + * widget failed to handle the update. */ public async feedStateUpdate(rawEvent: IRoomEvent): Promise { - const useUpdateState = (await this.getWidgetVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE); - if (rawEvent.state_key === undefined) throw new Error("Not a state event"); if ( (rawEvent.room_id === this.viewedRoomId || this.canUseRoomTimeline(rawEvent.room_id)) @@ -1183,8 +1181,8 @@ export class ClientWidgetApi extends EventEmitter { // Updates could race with the initial push of the room's state if (this.pushRoomStateTasks.size === 0) { // No initial push tasks are pending; safe to send immediately - if (useUpdateState) { - // Only send state updates when using UpdateState. Otherwise we will use SendEvent. + if ((await this.getWidgetVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE)) { + // Only send state updates when using UpdateState. Otherwise the SendEvent action will be responsible for state updates. await this.transport.send(WidgetApiToWidgetAction.UpdateState, { state: [rawEvent], });