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

Add event and message capabilities to RoomWidgetClient #2797

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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
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