Skip to content

Commit 8e6b102

Browse files
committed
Support MSC4157: delayed events via Widget API
Depends on matrix-org/matrix-widget-api#92
1 parent 991e954 commit 8e6b102

File tree

3 files changed

+277
-9
lines changed

3 files changed

+277
-9
lines changed

spec/unit/embedded.spec.ts

Lines changed: 177 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
IOpenIDCredentials,
3333
} from "matrix-widget-api";
3434

35-
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
35+
import { createRoomWidgetClient, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
3636
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
3737
import { SyncState } from "../../src/sync";
3838
import { ICapabilities } from "../../src/embedded";
@@ -59,8 +59,26 @@ class MockWidgetApi extends EventEmitter {
5959
public requestCapabilityToReceiveState = jest.fn();
6060
public requestCapabilityToSendToDevice = jest.fn();
6161
public requestCapabilityToReceiveToDevice = jest.fn();
62-
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
63-
public sendStateEvent = jest.fn();
62+
public sendRoomEvent = jest.fn(
63+
(eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
64+
delay === undefined && parentDelayId === undefined
65+
? { event_id: `$${Math.random()}` }
66+
: { delay_id: `id-${Math.random()}` },
67+
);
68+
public sendStateEvent = jest.fn(
69+
(
70+
eventType: string,
71+
stateKey: string,
72+
content: unknown,
73+
roomId?: string,
74+
delay?: number,
75+
parentDelayId?: string,
76+
) =>
77+
delay === undefined && parentDelayId === undefined
78+
? { event_id: `$${Math.random()}` }
79+
: { delay_id: `id-${Math.random()}` },
80+
);
81+
public updateDelayedEvent = jest.fn();
6482
public sendToDevice = jest.fn();
6583
public requestOpenIDConnectToken = jest.fn(() => {
6684
return testOIDCToken;
@@ -160,6 +178,162 @@ describe("RoomWidgetClient", () => {
160178
});
161179
});
162180

181+
describe("delayed events", () => {
182+
describe("when supported", () => {
183+
const doesServerSupportUnstableFeatureMock = jest.fn((feature) =>
184+
Promise.resolve(feature === "org.matrix.msc4140"),
185+
);
186+
187+
beforeAll(() => {
188+
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
189+
});
190+
191+
afterAll(() => {
192+
doesServerSupportUnstableFeatureMock.mockReset();
193+
});
194+
195+
it("sends delayed message events", async () => {
196+
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
197+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
198+
await client._unstable_sendDelayedEvent(
199+
"!1:example.org",
200+
{ delay: 2000 },
201+
null,
202+
"org.matrix.rageshake_request",
203+
{ request_id: 123 },
204+
);
205+
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
206+
"org.matrix.rageshake_request",
207+
{ request_id: 123 },
208+
"!1:example.org",
209+
2000,
210+
undefined,
211+
);
212+
});
213+
214+
it("sends child action delayed message events", async () => {
215+
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
216+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
217+
const parentDelayId = `id-${Math.random()}`;
218+
await client._unstable_sendDelayedEvent(
219+
"!1:example.org",
220+
{ parent_delay_id: parentDelayId },
221+
null,
222+
"org.matrix.rageshake_request",
223+
{ request_id: 123 },
224+
);
225+
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
226+
"org.matrix.rageshake_request",
227+
{ request_id: 123 },
228+
"!1:example.org",
229+
undefined,
230+
parentDelayId,
231+
);
232+
});
233+
234+
it("sends delayed state events", async () => {
235+
await makeClient({
236+
sendDelayedEvents: true,
237+
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
238+
});
239+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
240+
await client._unstable_sendDelayedStateEvent(
241+
"!1:example.org",
242+
{ delay: 2000 },
243+
"org.example.foo",
244+
{ hello: "world" },
245+
"bar",
246+
);
247+
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
248+
"org.example.foo",
249+
"bar",
250+
{ hello: "world" },
251+
"!1:example.org",
252+
2000,
253+
undefined,
254+
);
255+
});
256+
257+
it("sends child action delayed state events", async () => {
258+
await makeClient({
259+
sendDelayedEvents: true,
260+
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
261+
});
262+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
263+
const parentDelayId = `fg-${Math.random()}`;
264+
await client._unstable_sendDelayedStateEvent(
265+
"!1:example.org",
266+
{ parent_delay_id: parentDelayId },
267+
"org.example.foo",
268+
{ hello: "world" },
269+
"bar",
270+
);
271+
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
272+
"org.example.foo",
273+
"bar",
274+
{ hello: "world" },
275+
"!1:example.org",
276+
undefined,
277+
parentDelayId,
278+
);
279+
});
280+
281+
it("updates delayed events", async () => {
282+
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
283+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
284+
for (const action of [
285+
UpdateDelayedEventAction.Cancel,
286+
UpdateDelayedEventAction.Restart,
287+
UpdateDelayedEventAction.Send,
288+
]) {
289+
await client._unstable_updateDelayedEvent("id", action);
290+
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action);
291+
}
292+
});
293+
});
294+
295+
describe("when unsupported", () => {
296+
it("fails to send delayed message events", async () => {
297+
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
298+
await expect(
299+
client._unstable_sendDelayedEvent(
300+
"!1:example.org",
301+
{ delay: 2000 },
302+
null,
303+
"org.matrix.rageshake_request",
304+
{ request_id: 123 },
305+
),
306+
).rejects.toThrow("Server does not support");
307+
});
308+
309+
it("fails to send delayed state events", async () => {
310+
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
311+
await expect(
312+
client._unstable_sendDelayedStateEvent(
313+
"!1:example.org",
314+
{ delay: 2000 },
315+
"org.example.foo",
316+
{ hello: "world" },
317+
"bar",
318+
),
319+
).rejects.toThrow("Server does not support");
320+
});
321+
322+
it("fails to update delayed state events", async () => {
323+
await makeClient({});
324+
for (const action of [
325+
UpdateDelayedEventAction.Cancel,
326+
UpdateDelayedEventAction.Restart,
327+
UpdateDelayedEventAction.Send,
328+
]) {
329+
await expect(client._unstable_updateDelayedEvent("id", action)).rejects.toThrow(
330+
"Server does not support",
331+
);
332+
}
333+
});
334+
});
335+
});
336+
163337
describe("initialization", () => {
164338
it("requests permissions for specific message types", async () => {
165339
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });

src/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ export const UNSTABLE_MSC2666_SHARED_ROOMS = "uk.half-shot.msc2666";
534534
export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms";
535535
export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_mutual_rooms";
536536

537-
const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140";
537+
export const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140";
538538

539539
enum CrossSigningKeyType {
540540
MasterKey = "master_key",

src/embedded.ts

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,13 @@ import {
2626
} from "matrix-widget-api";
2727

2828
import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event";
29-
import { ISendEventResponse, SendDelayedEventRequestOpts, SendDelayedEventResponse } from "./@types/requests";
30-
import { EventType } from "./@types/event";
29+
import {
30+
ISendEventResponse,
31+
SendDelayedEventRequestOpts,
32+
SendDelayedEventResponse,
33+
UpdateDelayedEventAction,
34+
} from "./@types/requests";
35+
import { EventType, StateEvents } from "./@types/event";
3136
import { logger } from "./logger";
3237
import {
3338
MatrixClient,
@@ -36,6 +41,7 @@ import {
3641
IStartClientOpts,
3742
SendToDeviceContentMap,
3843
IOpenIDToken,
44+
UNSTABLE_MSC4140_DELAYED_EVENTS,
3945
} from "./client";
4046
import { SyncApi, SyncState } from "./sync";
4147
import { SlidingSyncSdk } from "./sliding-sync-sdk";
@@ -95,6 +101,20 @@ export interface ICapabilities {
95101
* @defaultValue false
96102
*/
97103
turnServers?: boolean;
104+
105+
/**
106+
* Whether this client needs to be able to send delayed events.
107+
* @experimental Part of MSC4140 & MSC4157
108+
* @defaultValue false
109+
*/
110+
sendDelayedEvents?: boolean;
111+
112+
/**
113+
* Whether this client needs to be able to update delayed events.
114+
* @experimental Part of MSC4140 & MSC4157
115+
* @defaultValue false
116+
*/
117+
updateDelayedEvents?: boolean;
98118
}
99119

100120
/**
@@ -162,6 +182,18 @@ export class RoomWidgetClient extends MatrixClient {
162182
);
163183
capabilities.sendToDevice?.forEach((eventType) => widgetApi.requestCapabilityToSendToDevice(eventType));
164184
capabilities.receiveToDevice?.forEach((eventType) => widgetApi.requestCapabilityToReceiveToDevice(eventType));
185+
if (
186+
capabilities.sendDelayedEvents &&
187+
(capabilities.sendEvent?.length ||
188+
capabilities.sendMessage === true ||
189+
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
190+
capabilities.sendState?.length)
191+
) {
192+
widgetApi.requestCapability(MatrixCapabilities.MSC4157SendDelayedEvent);
193+
}
194+
if (capabilities.updateDelayedEvents) {
195+
widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
196+
}
165197
if (capabilities.turnServers) {
166198
widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
167199
}
@@ -260,8 +292,17 @@ export class RoomWidgetClient extends MatrixClient {
260292
delayOpts?: SendDelayedEventRequestOpts,
261293
): Promise<ISendEventResponse | SendDelayedEventResponse> {
262294
if (delayOpts) {
263-
throw new Error("Delayed event sending via widgets is not implemented");
295+
// TODO: updatePendingEvent for delayed events?
296+
const response = await this.widgetApi.sendRoomEvent(
297+
event.getType(),
298+
event.getContent(),
299+
room.roomId,
300+
"delay" in delayOpts ? delayOpts.delay : undefined,
301+
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
302+
);
303+
return this.validateSendDelayedEventResponse(response);
264304
}
305+
265306
let response: ISendEventFromWidgetResponseData;
266307
try {
267308
response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId);
@@ -271,7 +312,7 @@ export class RoomWidgetClient extends MatrixClient {
271312
}
272313

273314
room.updatePendingEvent(event, EventStatus.SENT, response.event_id);
274-
return { event_id: response.event_id! };
315+
return this.validateSendEventResponse(response);
275316
}
276317

277318
public async sendStateEvent(
@@ -280,7 +321,60 @@ export class RoomWidgetClient extends MatrixClient {
280321
content: any,
281322
stateKey = "",
282323
): Promise<ISendEventResponse> {
283-
return (await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId)) as ISendEventResponse;
324+
const response = await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
325+
return this.validateSendEventResponse(response);
326+
}
327+
328+
/**
329+
* @experimental This currently relies on an unstable MSC (MSC4140).
330+
*/
331+
// eslint-disable-next-line
332+
public async _unstable_sendDelayedStateEvent<K extends keyof StateEvents>(
333+
roomId: string,
334+
delayOpts: SendDelayedEventRequestOpts,
335+
eventType: K,
336+
content: StateEvents[K],
337+
stateKey = "",
338+
): Promise<SendDelayedEventResponse> {
339+
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
340+
throw Error("Server does not support the delayed events API");
341+
}
342+
343+
const response = await this.widgetApi.sendStateEvent(
344+
eventType,
345+
stateKey,
346+
content,
347+
roomId,
348+
"delay" in delayOpts ? delayOpts.delay : undefined,
349+
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
350+
);
351+
return this.validateSendDelayedEventResponse(response);
352+
}
353+
354+
/**
355+
* @experimental This currently relies on an unstable MSC (MSC4140).
356+
*/
357+
// eslint-disable-next-line
358+
public async _unstable_updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<{}> {
359+
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
360+
throw Error("Server does not support the delayed events API");
361+
}
362+
363+
return await this.widgetApi.updateDelayedEvent(delayId, action);
364+
}
365+
366+
private validateSendEventResponse(response: ISendEventFromWidgetResponseData): ISendEventResponse {
367+
if (response.event_id === undefined) {
368+
throw new Error("'event_id' absent from response to an event request");
369+
}
370+
return { event_id: response.event_id };
371+
}
372+
373+
private validateSendDelayedEventResponse(response: ISendEventFromWidgetResponseData): SendDelayedEventResponse {
374+
if (response.delay_id === undefined) {
375+
throw new Error("'delay_id' absent from response to a delayed event request");
376+
}
377+
return { delay_id: response.delay_id };
284378
}
285379

286380
public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> {

0 commit comments

Comments
 (0)