1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Support MSC4157: delayed events via Widget API (#4311)

This commit is contained in:
Andrew Ferrazzutti
2024-08-01 10:17:52 -04:00
committed by GitHub
parent 89a9a7fa38
commit e10c362ef0
6 changed files with 341 additions and 15 deletions

View File

@ -60,7 +60,7 @@
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"loglevel": "^1.7.1", "loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1", "matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.6.0", "matrix-widget-api": "^1.8.1",
"oidc-client-ts": "^3.0.1", "oidc-client-ts": "^3.0.1",
"p-retry": "4", "p-retry": "4",
"sdp-transform": "^2.14.1", "sdp-transform": "^2.14.1",

View File

@ -268,7 +268,8 @@ describe("MSC4108SignInWithQR", () => {
it("should abort if device doesn't come up by timeout", async () => { it("should abort if device doesn't come up by timeout", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => { jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)(); (<Function>fn)();
return -1; // TODO: mock timers properly
return -1 as any;
}); });
jest.spyOn(Date, "now").mockImplementation(() => { jest.spyOn(Date, "now").mockImplementation(() => {
return 12345678 + mocked(setTimeout).mock.calls.length * 1000; return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
@ -320,7 +321,8 @@ describe("MSC4108SignInWithQR", () => {
it("should not send secrets if user cancels", async () => { it("should not send secrets if user cancels", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => { jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)(); (<Function>fn)();
return -1; // TODO: mock timers properly
return -1 as any;
}); });
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]); await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);

View File

@ -32,7 +32,7 @@ import {
IOpenIDCredentials, IOpenIDCredentials,
} from "matrix-widget-api"; } from "matrix-widget-api";
import { createRoomWidgetClient, MsgType } from "../../src/matrix"; import { createRoomWidgetClient, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client"; import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync"; import { SyncState } from "../../src/sync";
import { ICapabilities } from "../../src/embedded"; import { ICapabilities } from "../../src/embedded";
@ -59,8 +59,26 @@ class MockWidgetApi extends EventEmitter {
public requestCapabilityToReceiveState = jest.fn(); public requestCapabilityToReceiveState = jest.fn();
public requestCapabilityToSendToDevice = jest.fn(); public requestCapabilityToSendToDevice = jest.fn();
public requestCapabilityToReceiveToDevice = jest.fn(); public requestCapabilityToReceiveToDevice = jest.fn();
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` })); public sendRoomEvent = jest.fn(
public sendStateEvent = 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 updateDelayedEvent = jest.fn();
public sendToDevice = jest.fn(); public sendToDevice = jest.fn();
public requestOpenIDConnectToken = jest.fn(() => { public requestOpenIDConnectToken = jest.fn(() => {
return testOIDCToken; return testOIDCToken;
@ -125,6 +143,17 @@ describe("RoomWidgetClient", () => {
); );
}); });
it("send handles wrong field in response", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
delay_id: `id-${Math.random}`,
});
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow();
});
it("receives", async () => { it("receives", async () => {
const event = new MatrixEvent({ const event = new MatrixEvent({
type: "org.matrix.rageshake_request", type: "org.matrix.rageshake_request",
@ -160,6 +189,199 @@ 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({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
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({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
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({
sendDelayedEvents: true,
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
});
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
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({
sendDelayedEvents: true,
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
});
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
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,
);
});
it("send delayed message events handles wrong field in response", async () => {
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
event_id: `$${Math.random()}`,
});
await expect(
client._unstable_sendDelayedEvent(
"!1:example.org",
{ delay: 2000 },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
),
).rejects.toThrow();
});
it("send delayed state events handles wrong field in response", async () => {
await makeClient({
sendDelayedEvents: true,
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
});
widgetApi.sendStateEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
event_id: `$${Math.random()}`,
});
await expect(
client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ delay: 2000 },
"org.example.foo",
{ hello: "world" },
"bar",
),
).rejects.toThrow();
});
it("updates delayed events", async () => {
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
for (const action of [
UpdateDelayedEventAction.Cancel,
UpdateDelayedEventAction.Restart,
UpdateDelayedEventAction.Send,
]) {
await client._unstable_updateDelayedEvent("id", action);
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action);
}
});
});
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");
});
it("fails to update delayed state events", async () => {
await makeClient({});
for (const action of [
UpdateDelayedEventAction.Cancel,
UpdateDelayedEventAction.Restart,
UpdateDelayedEventAction.Send,
]) {
await expect(client._unstable_updateDelayedEvent("id", action)).rejects.toThrow(
"Server does not support",
);
}
});
});
});
describe("initialization", () => { describe("initialization", () => {
it("requests permissions for specific message types", async () => { it("requests permissions for specific message types", async () => {
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] }); await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
@ -211,6 +433,17 @@ describe("RoomWidgetClient", () => {
); );
}); });
it("send handles incorrect response", async () => {
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
widgetApi.sendStateEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
delay_id: `id-${Math.random}`,
});
await expect(
client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar"),
).rejects.toThrow();
});
it("receives", async () => { it("receives", async () => {
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] }); await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org"); expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");

View File

@ -535,7 +535,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_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms";
export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_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 { enum CrossSigningKeyType {
MasterKey = "master_key", MasterKey = "master_key",

View File

@ -26,8 +26,13 @@ import {
} from "matrix-widget-api"; } from "matrix-widget-api";
import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event"; import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event";
import { ISendEventResponse, SendDelayedEventRequestOpts, SendDelayedEventResponse } from "./@types/requests"; import {
import { EventType } from "./@types/event"; ISendEventResponse,
SendDelayedEventRequestOpts,
SendDelayedEventResponse,
UpdateDelayedEventAction,
} from "./@types/requests";
import { EventType, StateEvents } from "./@types/event";
import { logger } from "./logger"; import { logger } from "./logger";
import { import {
MatrixClient, MatrixClient,
@ -36,6 +41,7 @@ import {
IStartClientOpts, IStartClientOpts,
SendToDeviceContentMap, SendToDeviceContentMap,
IOpenIDToken, IOpenIDToken,
UNSTABLE_MSC4140_DELAYED_EVENTS,
} from "./client"; } from "./client";
import { SyncApi, SyncState } from "./sync"; import { SyncApi, SyncState } from "./sync";
import { SlidingSyncSdk } from "./sliding-sync-sdk"; import { SlidingSyncSdk } from "./sliding-sync-sdk";
@ -95,6 +101,20 @@ export interface ICapabilities {
* @defaultValue false * @defaultValue false
*/ */
turnServers?: boolean; turnServers?: boolean;
/**
* Whether this client needs to be able to send delayed events.
* @experimental Part of MSC4140 & MSC4157
* @defaultValue false
*/
sendDelayedEvents?: boolean;
/**
* Whether this client needs to be able to update delayed events.
* @experimental Part of MSC4140 & MSC4157
* @defaultValue false
*/
updateDelayedEvents?: boolean;
} }
/** /**
@ -162,6 +182,18 @@ export class RoomWidgetClient extends MatrixClient {
); );
capabilities.sendToDevice?.forEach((eventType) => widgetApi.requestCapabilityToSendToDevice(eventType)); capabilities.sendToDevice?.forEach((eventType) => widgetApi.requestCapabilityToSendToDevice(eventType));
capabilities.receiveToDevice?.forEach((eventType) => widgetApi.requestCapabilityToReceiveToDevice(eventType)); capabilities.receiveToDevice?.forEach((eventType) => widgetApi.requestCapabilityToReceiveToDevice(eventType));
if (
capabilities.sendDelayedEvents &&
(capabilities.sendEvent?.length ||
capabilities.sendMessage === true ||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
capabilities.sendState?.length)
) {
widgetApi.requestCapability(MatrixCapabilities.MSC4157SendDelayedEvent);
}
if (capabilities.updateDelayedEvents) {
widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
}
if (capabilities.turnServers) { if (capabilities.turnServers) {
widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers); widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
} }
@ -260,8 +292,17 @@ export class RoomWidgetClient extends MatrixClient {
delayOpts?: SendDelayedEventRequestOpts, delayOpts?: SendDelayedEventRequestOpts,
): Promise<ISendEventResponse | SendDelayedEventResponse> { ): Promise<ISendEventResponse | SendDelayedEventResponse> {
if (delayOpts) { 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; let response: ISendEventFromWidgetResponseData;
try { try {
response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId); response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId);
@ -270,6 +311,7 @@ export class RoomWidgetClient extends MatrixClient {
throw e; throw e;
} }
// This also checks for an event id on the response
room.updatePendingEvent(event, EventStatus.SENT, response.event_id); room.updatePendingEvent(event, EventStatus.SENT, response.event_id);
return { event_id: response.event_id! }; return { event_id: response.event_id! };
} }
@ -280,7 +322,56 @@ export class RoomWidgetClient extends MatrixClient {
content: any, content: any,
stateKey = "", stateKey = "",
): Promise<ISendEventResponse> { ): Promise<ISendEventResponse> {
return (await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId)) as ISendEventResponse; const response = await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
if (response.event_id === undefined) {
throw new Error("'event_id' absent from response to an event request");
}
return { event_id: response.event_id };
}
/**
* @experimental This currently relies on an unstable MSC (MSC4140).
*/
// eslint-disable-next-line
public async _unstable_sendDelayedStateEvent<K extends keyof StateEvents>(
roomId: string,
delayOpts: SendDelayedEventRequestOpts,
eventType: K,
content: StateEvents[K],
stateKey = "",
): Promise<SendDelayedEventResponse> {
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 validateSendDelayedEventResponse(response: ISendEventFromWidgetResponseData): SendDelayedEventResponse {
if (response.delay_id === undefined) {
throw new Error("'delay_id' absent from response to a delayed event request");
}
return { delay_id: response.delay_id };
}
/**
* @experimental This currently relies on an unstable MSC (MSC4140).
*/
// eslint-disable-next-line
public async _unstable_updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<{}> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
return await this.widgetApi.updateDelayedEvent(delayId, action);
} }
public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> { public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> {

View File

@ -4722,10 +4722,10 @@ matrix-mock-request@^2.5.0:
dependencies: dependencies:
expect "^28.1.0" expect "^28.1.0"
matrix-widget-api@^1.6.0: matrix-widget-api@^1.8.1:
version "1.7.0" version "1.8.1"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.7.0.tgz#ae3b44380f11bb03519d0bf0373dfc3341634667" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.8.1.tgz#90fe4814956a27d6c6cff55c2aa67da516722eb1"
integrity sha512-dzSnA5Va6CeIkyWs89xZty/uv38HLyfjOrHGbbEikCa2ZV0HTkUNtrBMKlrn4CRYyDJ6yoO/3ssRwiR0jJvOkQ== integrity sha512-oISJ9rmkfxNonrrWKUB2HCwj0i+J0b7zNGqwNudOjXj9Jsv7r8UWqowE7AEWyAsPp4IwW/hEvNeLbNo2wUsXwg==
dependencies: dependencies:
"@types/events" "^3.0.0" "@types/events" "^3.0.0"
events "^3.2.0" events "^3.2.0"