diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index d9ac5a62f0b..1fcc7844516 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -59,8 +59,25 @@ class MockWidgetApi extends EventEmitter { 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 sendRoomEvent = jest.fn( + (eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) => + delay === undefined && parentDelayId === undefined + ? { event_id: `$${Math.random()}` } + : { delay_id: `id-${Math.random()}` }, + ); + public sendStateEvent = jest.fn( + ( + eventType: string, + stateKey: string, + content: unknown, + roomId?: string, + delay?: number, + parentDelayId?: string, + ) => + delay === undefined && parentDelayId === undefined + ? { event_id: `$${Math.random()}` } + : { delay_id: `id-${Math.random()}` }, + ); public sendToDevice = jest.fn(); public requestOpenIDConnectToken = jest.fn(() => { return testOIDCToken; @@ -160,6 +177,134 @@ describe("RoomWidgetClient", () => { }); }); + describe("delayed events", () => { + describe("when supported", () => { + const doesServerSupportUnstableFeatureMock = jest.fn((feature) => + Promise.resolve(feature === "org.matrix.msc4140"), + ); + + beforeAll(() => { + MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock; + }); + + afterAll(() => { + doesServerSupportUnstableFeatureMock.mockReset(); + }); + + it("sends delayed message events", 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._unstable_sendDelayedEvent( + "!1:example.org", + { delay: 2000 }, + null, + "org.matrix.rageshake_request", + { request_id: 123 }, + ); + expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith( + "org.matrix.rageshake_request", + { request_id: 123 }, + "!1:example.org", + 2000, + undefined, + ); + }); + + it("sends child action delayed message events", async () => { + await makeClient({ sendEvent: ["org.matrix.rageshake_request"] }); + expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org"); + expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith("org.matrix.rageshake_request"); + const parentDelayId = `id-${Math.random()}`; + await client._unstable_sendDelayedEvent( + "!1:example.org", + { parent_delay_id: parentDelayId }, + null, + "org.matrix.rageshake_request", + { request_id: 123 }, + ); + expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith( + "org.matrix.rageshake_request", + { request_id: 123 }, + "!1:example.org", + undefined, + parentDelayId, + ); + }); + + it("sends delayed state events", 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._unstable_sendDelayedStateEvent( + "!1:example.org", + { delay: 2000 }, + "org.example.foo", + { hello: "world" }, + "bar", + ); + expect(widgetApi.sendStateEvent).toHaveBeenCalledWith( + "org.example.foo", + "bar", + { hello: "world" }, + "!1:example.org", + 2000, + undefined, + ); + }); + + it("sends child action delayed state events", 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"); + const parentDelayId = `fg-${Math.random()}`; + await client._unstable_sendDelayedStateEvent( + "!1:example.org", + { parent_delay_id: parentDelayId }, + "org.example.foo", + { hello: "world" }, + "bar", + ); + expect(widgetApi.sendStateEvent).toHaveBeenCalledWith( + "org.example.foo", + "bar", + { hello: "world" }, + "!1:example.org", + undefined, + parentDelayId, + ); + }); + }); + + describe("when unsupported", () => { + it("fails to send delayed message events", async () => { + await makeClient({ sendEvent: ["org.matrix.rageshake_request"] }); + await expect( + client._unstable_sendDelayedEvent( + "!1:example.org", + { delay: 2000 }, + null, + "org.matrix.rageshake_request", + { request_id: 123 }, + ), + ).rejects.toThrow("Server does not support"); + }); + + it("fails to send delayed state events", async () => { + await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); + await expect( + client._unstable_sendDelayedStateEvent( + "!1:example.org", + { delay: 2000 }, + "org.example.foo", + { hello: "world" }, + "bar", + ), + ).rejects.toThrow("Server does not support"); + }); + }); + }); + describe("initialization", () => { it("requests permissions for specific message types", async () => { await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] }); diff --git a/src/client.ts b/src/client.ts index 2b6e7bed056..3769df2c2eb 100644 --- a/src/client.ts +++ b/src/client.ts @@ -531,7 +531,7 @@ export const UNSTABLE_MSC2666_SHARED_ROOMS = "uk.half-shot.msc2666"; export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms"; export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_mutual_rooms"; -const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140"; +export const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140"; enum CrossSigningKeyType { MasterKey = "master_key", diff --git a/src/embedded.ts b/src/embedded.ts index 91d602e34cb..a92e34202f3 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -27,7 +27,7 @@ import { import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event"; import { ISendEventResponse, SendDelayedEventRequestOpts, SendDelayedEventResponse } from "./@types/requests"; -import { EventType } from "./@types/event"; +import { EventType, StateEvents } from "./@types/event"; import { logger } from "./logger"; import { MatrixClient, @@ -36,6 +36,7 @@ import { IStartClientOpts, SendToDeviceContentMap, IOpenIDToken, + UNSTABLE_MSC4140_DELAYED_EVENTS, } from "./client"; import { SyncApi, SyncState } from "./sync"; import { SlidingSyncSdk } from "./sliding-sync-sdk"; @@ -260,8 +261,17 @@ export class RoomWidgetClient extends MatrixClient { delayOpts?: SendDelayedEventRequestOpts, ): Promise { if (delayOpts) { - throw new Error("Delayed event sending via widgets is not implemented"); + // TODO: updatePendingEvent for delayed events? + const response = await this.widgetApi.sendRoomEvent( + event.getType(), + event.getContent(), + room.roomId, + "delay" in delayOpts ? delayOpts.delay : undefined, + "parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined, + ); + return this.validateSendDelayedEventResponse(response); } + let response: ISendEventFromWidgetResponseData; try { response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId); @@ -271,7 +281,7 @@ export class RoomWidgetClient extends MatrixClient { } room.updatePendingEvent(event, EventStatus.SENT, response.event_id); - return { event_id: response.event_id }; + return this.validateSendEventResponse(response); } public async sendStateEvent( @@ -280,7 +290,48 @@ export class RoomWidgetClient extends MatrixClient { content: any, stateKey = "", ): Promise { - return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId); + const response = await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId); + return this.validateSendEventResponse(response); + } + + /** + * @experimental This currently relies on an unstable MSC (MSC4140). + */ + // eslint-disable-next-line + public async _unstable_sendDelayedStateEvent( + roomId: string, + delayOpts: SendDelayedEventRequestOpts, + eventType: K, + content: StateEvents[K], + stateKey = "", + ): Promise { + if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) { + throw Error("Server does not support the delayed events API"); + } + + const response = await this.widgetApi.sendStateEvent( + eventType, + stateKey, + content, + roomId, + "delay" in delayOpts ? delayOpts.delay : undefined, + "parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined, + ); + return this.validateSendDelayedEventResponse(response); + } + + private validateSendEventResponse(response: ISendEventFromWidgetResponseData): ISendEventResponse { + if (response.event_id === undefined) { + throw new Error("'event_id' absent from response to an event request"); + } + return { event_id: response.event_id }; + } + + private validateSendDelayedEventResponse(response: ISendEventFromWidgetResponseData): SendDelayedEventResponse { + if (!response.delay_id) { + throw new Error("'delay_id' absent from response to a delayed event request"); + } + return { delay_id: response.delay_id }; } public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> {