Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Target widget actions at a specific room #2670

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary now and it wasn't before?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the timeline capability is a requirement for accessing a specific room, even if that room is normally what you'd think of as "the current room". The widget API rejects any requests directed at a specific room if the widget doesn't have this capability.

}
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`);
Comment on lines +231 to +232
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think it might be nicer for this to be in the if followed by return

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning early would mean that we skip the ack call though, which is still something we should do

}

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