Skip to content

Commit e10c362

Browse files
authored
Support MSC4157: delayed events via Widget API (#4311)
1 parent 89a9a7f commit e10c362

File tree

6 files changed

+341
-15
lines changed

6 files changed

+341
-15
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"jwt-decode": "^4.0.0",
6161
"loglevel": "^1.7.1",
6262
"matrix-events-sdk": "0.0.1",
63-
"matrix-widget-api": "^1.6.0",
63+
"matrix-widget-api": "^1.8.1",
6464
"oidc-client-ts": "^3.0.1",
6565
"p-retry": "4",
6666
"sdp-transform": "^2.14.1",

spec/integ/rendezvous/MSC4108SignInWithQR.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,8 @@ describe("MSC4108SignInWithQR", () => {
268268
it("should abort if device doesn't come up by timeout", async () => {
269269
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
270270
(<Function>fn)();
271-
return -1;
271+
// TODO: mock timers properly
272+
return -1 as any;
272273
});
273274
jest.spyOn(Date, "now").mockImplementation(() => {
274275
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
@@ -320,7 +321,8 @@ describe("MSC4108SignInWithQR", () => {
320321
it("should not send secrets if user cancels", async () => {
321322
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
322323
(<Function>fn)();
323-
return -1;
324+
// TODO: mock timers properly
325+
return -1 as any;
324326
});
325327

326328
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);

spec/unit/embedded.spec.ts

Lines changed: 236 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;
@@ -125,6 +143,17 @@ describe("RoomWidgetClient", () => {
125143
);
126144
});
127145

146+
it("send handles wrong field in response", async () => {
147+
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
148+
widgetApi.sendRoomEvent.mockResolvedValueOnce({
149+
room_id: "!1:example.org",
150+
delay_id: `id-${Math.random}`,
151+
});
152+
await expect(
153+
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
154+
).rejects.toThrow();
155+
});
156+
128157
it("receives", async () => {
129158
const event = new MatrixEvent({
130159
type: "org.matrix.rageshake_request",
@@ -160,6 +189,199 @@ describe("RoomWidgetClient", () => {
160189
});
161190
});
162191

192+
describe("delayed events", () => {
193+
describe("when supported", () => {
194+
const doesServerSupportUnstableFeatureMock = jest.fn((feature) =>
195+
Promise.resolve(feature === "org.matrix.msc4140"),
196+
);
197+
198+
beforeAll(() => {
199+
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
200+
});
201+
202+
afterAll(() => {
203+
doesServerSupportUnstableFeatureMock.mockReset();
204+
});
205+
206+
it("sends delayed message events", async () => {
207+
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
208+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
209+
await client._unstable_sendDelayedEvent(
210+
"!1:example.org",
211+
{ delay: 2000 },
212+
null,
213+
"org.matrix.rageshake_request",
214+
{ request_id: 123 },
215+
);
216+
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
217+
"org.matrix.rageshake_request",
218+
{ request_id: 123 },
219+
"!1:example.org",
220+
2000,
221+
undefined,
222+
);
223+
});
224+
225+
it("sends child action delayed message events", async () => {
226+
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
227+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
228+
const parentDelayId = `id-${Math.random()}`;
229+
await client._unstable_sendDelayedEvent(
230+
"!1:example.org",
231+
{ parent_delay_id: parentDelayId },
232+
null,
233+
"org.matrix.rageshake_request",
234+
{ request_id: 123 },
235+
);
236+
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
237+
"org.matrix.rageshake_request",
238+
{ request_id: 123 },
239+
"!1:example.org",
240+
undefined,
241+
parentDelayId,
242+
);
243+
});
244+
245+
it("sends delayed state events", async () => {
246+
await makeClient({
247+
sendDelayedEvents: true,
248+
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
249+
});
250+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
251+
await client._unstable_sendDelayedStateEvent(
252+
"!1:example.org",
253+
{ delay: 2000 },
254+
"org.example.foo",
255+
{ hello: "world" },
256+
"bar",
257+
);
258+
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
259+
"org.example.foo",
260+
"bar",
261+
{ hello: "world" },
262+
"!1:example.org",
263+
2000,
264+
undefined,
265+
);
266+
});
267+
268+
it("sends child action delayed state events", async () => {
269+
await makeClient({
270+
sendDelayedEvents: true,
271+
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
272+
});
273+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
274+
const parentDelayId = `fg-${Math.random()}`;
275+
await client._unstable_sendDelayedStateEvent(
276+
"!1:example.org",
277+
{ parent_delay_id: parentDelayId },
278+
"org.example.foo",
279+
{ hello: "world" },
280+
"bar",
281+
);
282+
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
283+
"org.example.foo",
284+
"bar",
285+
{ hello: "world" },
286+
"!1:example.org",
287+
undefined,
288+
parentDelayId,
289+
);
290+
});
291+
292+
it("send delayed message events handles wrong field in response", async () => {
293+
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
294+
widgetApi.sendRoomEvent.mockResolvedValueOnce({
295+
room_id: "!1:example.org",
296+
event_id: `$${Math.random()}`,
297+
});
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();
307+
});
308+
309+
it("send delayed state events handles wrong field in response", async () => {
310+
await makeClient({
311+
sendDelayedEvents: true,
312+
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
313+
});
314+
widgetApi.sendStateEvent.mockResolvedValueOnce({
315+
room_id: "!1:example.org",
316+
event_id: `$${Math.random()}`,
317+
});
318+
await expect(
319+
client._unstable_sendDelayedStateEvent(
320+
"!1:example.org",
321+
{ delay: 2000 },
322+
"org.example.foo",
323+
{ hello: "world" },
324+
"bar",
325+
),
326+
).rejects.toThrow();
327+
});
328+
329+
it("updates delayed events", async () => {
330+
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
331+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
332+
for (const action of [
333+
UpdateDelayedEventAction.Cancel,
334+
UpdateDelayedEventAction.Restart,
335+
UpdateDelayedEventAction.Send,
336+
]) {
337+
await client._unstable_updateDelayedEvent("id", action);
338+
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action);
339+
}
340+
});
341+
});
342+
343+
describe("when unsupported", () => {
344+
it("fails to send delayed message events", async () => {
345+
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
346+
await expect(
347+
client._unstable_sendDelayedEvent(
348+
"!1:example.org",
349+
{ delay: 2000 },
350+
null,
351+
"org.matrix.rageshake_request",
352+
{ request_id: 123 },
353+
),
354+
).rejects.toThrow("Server does not support");
355+
});
356+
357+
it("fails to send delayed state events", async () => {
358+
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
359+
await expect(
360+
client._unstable_sendDelayedStateEvent(
361+
"!1:example.org",
362+
{ delay: 2000 },
363+
"org.example.foo",
364+
{ hello: "world" },
365+
"bar",
366+
),
367+
).rejects.toThrow("Server does not support");
368+
});
369+
370+
it("fails to update delayed state events", async () => {
371+
await makeClient({});
372+
for (const action of [
373+
UpdateDelayedEventAction.Cancel,
374+
UpdateDelayedEventAction.Restart,
375+
UpdateDelayedEventAction.Send,
376+
]) {
377+
await expect(client._unstable_updateDelayedEvent("id", action)).rejects.toThrow(
378+
"Server does not support",
379+
);
380+
}
381+
});
382+
});
383+
});
384+
163385
describe("initialization", () => {
164386
it("requests permissions for specific message types", async () => {
165387
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
@@ -211,6 +433,17 @@ describe("RoomWidgetClient", () => {
211433
);
212434
});
213435

436+
it("send handles incorrect response", async () => {
437+
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
438+
widgetApi.sendStateEvent.mockResolvedValueOnce({
439+
room_id: "!1:example.org",
440+
delay_id: `id-${Math.random}`,
441+
});
442+
await expect(
443+
client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar"),
444+
).rejects.toThrow();
445+
});
446+
214447
it("receives", async () => {
215448
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
216449
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");

src/client.ts

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

538-
const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140";
538+
export const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140";
539539

540540
enum CrossSigningKeyType {
541541
MasterKey = "master_key",

0 commit comments

Comments
 (0)