Skip to content

Commit

Permalink
Target widget actions at a specific room (#2670)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
robintown authored Sep 16, 2022
1 parent 6fc9827 commit de69445
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 34 deletions.
23 changes: 12 additions & 11 deletions spec/unit/embedded.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve));
Expand All @@ -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 () => {
Expand All @@ -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);
});
});

Expand Down Expand Up @@ -257,7 +258,7 @@ describe("RoomWidgetClient", () => {
const emittedServer = new Promise<IClientTurnServer[]>(resolve =>
client.once(ClientEvent.TurnServers, resolve),
);
emitServer2();
emitServer2!();
expect(await emittedServer).toEqual([clientServer2]);
expect(client.getTurnServers()).toEqual([clientServer2]);
});
Expand Down
58 changes: 35 additions & 23 deletions src/embedded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<void> {
Expand Down Expand Up @@ -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<IEvent>));

await this.syncApi.injectRoomEvents(this.room, [], events);
events.forEach(event => {
Expand Down Expand Up @@ -157,8 +161,7 @@ export class RoomWidgetClient extends MatrixClient {
content: any,
stateKey = "",
): Promise<ISendEventResponse> {
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(
Expand Down Expand Up @@ -215,11 +218,20 @@ export class RoomWidgetClient extends MatrixClient {

private onEvent = async (ev: CustomEvent<ISendEventToWidgetActionRequest>) => {
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<IEvent>);
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);
};

Expand All @@ -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, {}, "", "");
Expand Down

0 comments on commit de69445

Please sign in to comment.