Skip to content

Commit

Permalink
Merge pull request #2797 from robintown/matryoshka-events
Browse files Browse the repository at this point in the history
Add event and message capabilities to RoomWidgetClient
  • Loading branch information
robintown authored Oct 24, 2022
2 parents eddd0ca + 8cd5aac commit 35f697a
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 8 deletions.
67 changes: 66 additions & 1 deletion spec/unit/embedded.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
ITurnServer,
} from "matrix-widget-api";

import { createRoomWidgetClient } from "../../src/matrix";
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync";
import { ICapabilities } from "../../src/embedded";
Expand All @@ -43,10 +43,15 @@ class MockWidgetApi extends EventEmitter {
public requestCapability = jest.fn();
public requestCapabilities = jest.fn();
public requestCapabilityForRoomTimeline = jest.fn();
public requestCapabilityToSendEvent = jest.fn();
public requestCapabilityToReceiveEvent = jest.fn();
public requestCapabilityToSendMessage = jest.fn();
public requestCapabilityToReceiveMessage = jest.fn();
public requestCapabilityToSendState = jest.fn();
public requestCapabilityToReceiveState = jest.fn();
public requestCapabilityToSendToDevice = jest.fn();
public requestCapabilityToReceiveToDevice = jest.fn();
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
public sendStateEvent = jest.fn();
public sendToDevice = jest.fn();
public readStateEvents = jest.fn(() => []);
Expand Down Expand Up @@ -75,6 +80,66 @@ describe("RoomWidgetClient", () => {
await client.startClient();
};

describe("events", () => {
it("sends", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
await client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 });
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
"org.matrix.rageshake_request", { request_id: 123 }, "!1:example.org",
);
});

it("receives", async () => {
const event = new MatrixEvent({
type: "org.matrix.rageshake_request",
event_id: "$pduhfiidph",
room_id: "!1:example.org",
sender: "@alice:example.org",
content: { request_id: 123 },
}).getEffectiveEvent();

await makeClient({ receiveEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");

const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
);

// The client should've emitted about the received event
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
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).not.toBeNull();
expect(room!.getLiveTimeline().getEvents().map(e => e.getEffectiveEvent())).toEqual([event]);
});
});

describe("messages", () => {
it("requests permissions for specific message types", async () => {
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith(MsgType.Text);
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith(MsgType.Text);
});

it("requests permissions for all message types", async () => {
await makeClient({ sendMessage: true, receiveMessage: true });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith();
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith();
});

// No point in testing sending and receiving since it's done exactly the
// same way as non-message events
});

describe("state events", () => {
const event = new MatrixEvent({
type: "org.example.foo",
Expand Down
5 changes: 2 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3980,9 +3980,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param room
* @param event
* @returns {Promise} returns a promise which resolves with the result of the send request
* @private
*/
private encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse> {
protected encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse> {
let cancelled = false;
// Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections,
// so that we can handle synchronous and asynchronous exceptions with the
Expand Down Expand Up @@ -4107,7 +4106,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.isRoomEncrypted(roomId) ? EventType.RoomMessageEncrypted : eventType;
}

private updatePendingEventStatus(room: Room | null, event: MatrixEvent, newStatus: EventStatus) {
protected updatePendingEventStatus(room: Room | null, event: MatrixEvent, newStatus: EventStatus) {
if (room) {
room.updatePendingEvent(event, newStatus);
} else {
Expand Down
90 changes: 86 additions & 4 deletions src/embedded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ import {
IWidgetApiAcknowledgeResponseData,
ISendEventToWidgetActionRequest,
ISendToDeviceToWidgetActionRequest,
ISendEventFromWidgetResponseData,
} from "matrix-widget-api";

import type { IEvent, IContent } from "./models/event";
import { IEvent, IContent, EventStatus } from "./models/event";
import { ISendEventResponse } from "./@types/requests";
import { EventType } from "./@types/event";
import { logger } from "./logger";
Expand All @@ -44,17 +45,56 @@ interface IStateEventRequest {
}

export interface ICapabilities {
// TODO: Add fields for messages and other non-state events

/**
* Event types that this client expects to send.
*/
sendEvent?: string[];
/**
* Event types that this client expects to receive.
*/
receiveEvent?: string[];

/**
* Message types that this client expects to send, or true for all message
* types.
*/
sendMessage?: string[] | true;
/**
* Message types that this client expects to receive, or true for all
* message types.
*/
receiveMessage?: string[] | true;

/**
* Types of state events that this client expects to send.
*/
sendState?: IStateEventRequest[];
/**
* Types of state events that this client expects to receive.
*/
receiveState?: IStateEventRequest[];

/**
* To-device event types that this client expects to send.
*/
sendToDevice?: string[];
/**
* To-device event types that this client expects to receive.
*/
receiveToDevice?: string[];

/**
* Whether this client needs access to TURN servers.
* @default false
*/
turnServers?: boolean;
}

/**
* A MatrixClient that routes its requests through the widget API instead of the
* real CS API.
* @experimental This class is considered unstable!
*/
export class RoomWidgetClient extends MatrixClient {
private room: Room;
private widgetApiReady = new Promise<void>(resolve => this.widgetApi.once("ready", resolve));
Expand All @@ -70,9 +110,38 @@ export class RoomWidgetClient extends MatrixClient {
super(opts);

// Request capabilities for the functionality this client needs to support
if (capabilities.sendState?.length || capabilities.receiveState?.length) {
if (
capabilities.sendEvent?.length
|| capabilities.receiveEvent?.length
|| capabilities.sendMessage === true
|| (Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length)
|| capabilities.receiveMessage === true
|| (Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length)
|| capabilities.sendState?.length
|| capabilities.receiveState?.length
) {
widgetApi.requestCapabilityForRoomTimeline(roomId);
}
capabilities.sendEvent?.forEach(eventType =>
widgetApi.requestCapabilityToSendEvent(eventType),
);
capabilities.receiveEvent?.forEach(eventType =>
widgetApi.requestCapabilityToReceiveEvent(eventType),
);
if (capabilities.sendMessage === true) {
widgetApi.requestCapabilityToSendMessage();
} else if (Array.isArray(capabilities.sendMessage)) {
capabilities.sendMessage.forEach(msgType =>
widgetApi.requestCapabilityToSendMessage(msgType),
);
}
if (capabilities.receiveMessage === true) {
widgetApi.requestCapabilityToReceiveMessage();
} else if (Array.isArray(capabilities.receiveMessage)) {
capabilities.receiveMessage.forEach(msgType =>
widgetApi.requestCapabilityToReceiveMessage(msgType),
);
}
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
widgetApi.requestCapabilityToSendState(eventType, stateKey),
);
Expand Down Expand Up @@ -155,6 +224,19 @@ export class RoomWidgetClient extends MatrixClient {
throw new Error(`Unknown room: ${roomIdOrAlias}`);
}

protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse> {
let response: ISendEventFromWidgetResponseData;
try {
response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId);
} catch (e) {
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
throw e;
}

room.updatePendingEvent(event, EventStatus.SENT, response.event_id);
return { event_id: response.event_id };
}

public async sendStateEvent(
roomId: string,
eventType: string,
Expand Down

0 comments on commit 35f697a

Please sign in to comment.