From de694459be1505dbf4ce96d5afa912d7dceb2987 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 16 Sep 2022 10:26:03 -0400 Subject: [PATCH] Target widget actions at a specific room (#2670) Otherwise, the RoomWidgetClient class can end up accidentally sending and receiving events from rooms it didn't intend to, if it's an always-on-screen widget. --- spec/unit/embedded.spec.ts | 23 +++++++-------- src/embedded.ts | 58 +++++++++++++++++++++++--------------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index edac107b976..f704e847d13 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -42,6 +42,7 @@ class MockWidgetApi extends EventEmitter { public start = jest.fn(); public requestCapability = jest.fn(); public requestCapabilities = jest.fn(); + public requestCapabilityForRoomTimeline = jest.fn(); public requestCapabilityToSendState = jest.fn(); public requestCapabilityToReceiveState = jest.fn(); public requestCapabilityToSendToDevice = jest.fn(); @@ -86,20 +87,17 @@ describe("RoomWidgetClient", () => { it("sends", async () => { await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); + expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org"); expect(widgetApi.requestCapabilityToSendState).toHaveBeenCalledWith("org.example.foo", "bar"); await client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar"); - expect(widgetApi.sendStateEvent).toHaveBeenCalledWith("org.example.foo", "bar", { hello: "world" }); - }); - - it("refuses to send to other rooms", async () => { - await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); - expect(widgetApi.requestCapabilityToSendState).toHaveBeenCalledWith("org.example.foo", "bar"); - await expect(client.sendStateEvent("!2:example.org", "org.example.foo", { hello: "world" }, "bar")) - .rejects.toBeDefined(); + expect(widgetApi.sendStateEvent).toHaveBeenCalledWith( + "org.example.foo", "bar", { hello: "world" }, "!1:example.org", + ); }); it("receives", async () => { await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); + expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org"); expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar"); const emittedEvent = new Promise(resolve => client.once(ClientEvent.Event, resolve)); @@ -114,7 +112,8 @@ describe("RoomWidgetClient", () => { expect(await emittedSync).toEqual(SyncState.Syncing); // It should've also inserted the event into the room object const room = client.getRoom("!1:example.org"); - expect(room.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event); + expect(room).not.toBeNull(); + expect(room!.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event); }); it("backfills", async () => { @@ -125,10 +124,12 @@ describe("RoomWidgetClient", () => { ); await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); + expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org"); expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar"); const room = client.getRoom("!1:example.org"); - expect(room.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event); + expect(room).not.toBeNull(); + expect(room!.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event); }); }); @@ -257,7 +258,7 @@ describe("RoomWidgetClient", () => { const emittedServer = new Promise(resolve => client.once(ClientEvent.TurnServers, resolve), ); - emitServer2(); + emitServer2!(); expect(await emittedServer).toEqual([clientServer2]); expect(client.getTurnServers()).toEqual([clientServer2]); }); diff --git a/src/embedded.ts b/src/embedded.ts index 48a0b8dd448..4a62810687f 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -24,6 +24,7 @@ import { ISendToDeviceToWidgetActionRequest, } from "matrix-widget-api"; +import type { IEvent, IContent } from "./models/event"; import { ISendEventResponse } from "./@types/requests"; import { EventType } from "./@types/event"; import { logger } from "./logger"; @@ -69,27 +70,30 @@ export class RoomWidgetClient extends MatrixClient { super(opts); // Request capabilities for the functionality this client needs to support - this.capabilities.sendState?.forEach(({ eventType, stateKey }) => - this.widgetApi.requestCapabilityToSendState(eventType, stateKey), + if (capabilities.sendState?.length || capabilities.receiveState?.length) { + widgetApi.requestCapabilityForRoomTimeline(roomId); + } + capabilities.sendState?.forEach(({ eventType, stateKey }) => + widgetApi.requestCapabilityToSendState(eventType, stateKey), ); - this.capabilities.receiveState?.forEach(({ eventType, stateKey }) => - this.widgetApi.requestCapabilityToReceiveState(eventType, stateKey), + capabilities.receiveState?.forEach(({ eventType, stateKey }) => + widgetApi.requestCapabilityToReceiveState(eventType, stateKey), ); - this.capabilities.sendToDevice?.forEach(eventType => - this.widgetApi.requestCapabilityToSendToDevice(eventType), + capabilities.sendToDevice?.forEach(eventType => + widgetApi.requestCapabilityToSendToDevice(eventType), ); - this.capabilities.receiveToDevice?.forEach(eventType => - this.widgetApi.requestCapabilityToReceiveToDevice(eventType), + capabilities.receiveToDevice?.forEach(eventType => + widgetApi.requestCapabilityToReceiveToDevice(eventType), ); - if (this.capabilities.turnServers) { - this.widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers); + if (capabilities.turnServers) { + widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers); } - this.widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent); - this.widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice); + widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent); + widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice); // Open communication with the host - this.widgetApi.start(); + widgetApi.start(); } public async startClient(opts: IStartClientOpts = {}): Promise { @@ -121,8 +125,8 @@ export class RoomWidgetClient extends MatrixClient { // so it doesn't really matter what order we inject them in await Promise.all( this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => { - const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey); - const events = rawEvents.map(rawEvent => new MatrixEvent(rawEvent)); + const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]); + const events = rawEvents.map(rawEvent => new MatrixEvent(rawEvent as Partial)); await this.syncApi.injectRoomEvents(this.room, [], events); events.forEach(event => { @@ -157,8 +161,7 @@ export class RoomWidgetClient extends MatrixClient { content: any, stateKey = "", ): Promise { - if (roomId !== this.roomId) throw new Error(`Can't send events to ${roomId}`); - return await this.widgetApi.sendStateEvent(eventType, stateKey, content); + return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId); } public async sendToDevice( @@ -215,11 +218,20 @@ export class RoomWidgetClient extends MatrixClient { private onEvent = async (ev: CustomEvent) => { ev.preventDefault(); - const event = new MatrixEvent(ev.detail.data); - await this.syncApi.injectRoomEvents(this.room, [], [event]); - this.emit(ClientEvent.Event, event); - this.setSyncState(SyncState.Syncing); - logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); + + // Verify the room ID matches, since it's possible for the client to + // send us events from other rooms if this widget is always on screen + if (ev.detail.data.room_id === this.roomId) { + const event = new MatrixEvent(ev.detail.data as Partial); + await this.syncApi.injectRoomEvents(this.room, [], [event]); + this.emit(ClientEvent.Event, event); + this.setSyncState(SyncState.Syncing); + logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); + } else { + const { event_id: eventId, room_id: roomId } = ev.detail.data; + logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`); + } + await this.ack(ev); }; @@ -229,7 +241,7 @@ export class RoomWidgetClient extends MatrixClient { const event = new MatrixEvent({ type: ev.detail.data.type, sender: ev.detail.data.sender, - content: ev.detail.data.content, + content: ev.detail.data.content as IContent, }); // Mark the event as encrypted if it was, using fake contents and keys since those are unknown to us if (ev.detail.data.encrypted) event.makeEncrypted(EventType.RoomMessageEncrypted, {}, "", "");