Give RoomWidgetClient the ability to send and receive sticky events (#5142)
* Give RoomWidgetClient the ability to send and receive sticky events * linter * Fix existing tests * Add tests for sticky event support in embedded clients * Update sticky event widget capability identifiers In matrix-widget-api 0.16.1 they are updated to use the new unstable prefix from MSC4407. * Explicitly require matrix-widget-api ≥ 1.16.1 * remove TODO comment * simplify type lint checks This is needed for EW donwstream tests. Otherwise it will through: Error: matrix-js-sdk/src/embedded.ts(417,21): error TS2345: Argument of type 'string | number | boolean | string[]' is not assignable to parameter of type 'number'. --------- Co-authored-by: Timo K <toger5@hotmail.de>
This commit is contained in:
+1
-1
@@ -56,7 +56,7 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.14.0",
|
||||
"matrix-widget-api": "^1.16.1",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "7",
|
||||
"sdp-transform": "^3.0.0",
|
||||
|
||||
+171
-1
@@ -32,7 +32,14 @@ import {
|
||||
type IRoomEvent,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
|
||||
import {
|
||||
createRoomWidgetClient,
|
||||
EventType,
|
||||
type IEvent,
|
||||
MatrixError,
|
||||
MsgType,
|
||||
UpdateDelayedEventAction,
|
||||
} from "../../src/matrix";
|
||||
import { MatrixClient, ClientEvent, type ITurnServer as IClientTurnServer } from "../../src/client";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
|
||||
@@ -42,6 +49,7 @@ import { sleep } from "../../src/utils";
|
||||
import { SlidingSync } from "../../src/sliding-sync";
|
||||
import { logger } from "../../src/logger";
|
||||
import { flushPromises } from "../test-utils/flushPromises";
|
||||
import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../../src/models/room-sticky-events";
|
||||
|
||||
const testOIDCToken = {
|
||||
access_token: "12345678",
|
||||
@@ -49,6 +57,7 @@ const testOIDCToken = {
|
||||
matrix_server_name: "homeserver.oabc",
|
||||
token_type: "Bearer",
|
||||
};
|
||||
|
||||
class MockWidgetApi extends EventEmitter {
|
||||
public start = vi.fn().mockResolvedValue(undefined);
|
||||
public getClientVersions = vi.fn();
|
||||
@@ -167,6 +176,9 @@ describe("RoomWidgetClient", () => {
|
||||
"org.matrix.rageshake_request",
|
||||
{ request_id: 123 },
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -422,6 +434,7 @@ describe("RoomWidgetClient", () => {
|
||||
"!1:example.org",
|
||||
2000,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -442,6 +455,7 @@ describe("RoomWidgetClient", () => {
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
parentDelayId,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -855,6 +869,162 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sticky events", () => {
|
||||
describe("when supported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
|
||||
Promise.resolve(feature === "org.matrix.msc4354"),
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("requests capabilities when set", async () => {
|
||||
await makeClient({ sendSticky: true, receiveSticky: true });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407ReceiveStickyEvent);
|
||||
});
|
||||
|
||||
it("does not request capabilities when unset", async () => {
|
||||
await makeClient({});
|
||||
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(
|
||||
MatrixCapabilities.MSC4407ReceiveStickyEvent,
|
||||
);
|
||||
});
|
||||
|
||||
it("sends", async () => {
|
||||
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith(EventType.RTCMembership);
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
await client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
|
||||
msc4354_sticky_key: "test",
|
||||
});
|
||||
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
|
||||
EventType.RTCMembership,
|
||||
{ msc4354_sticky_key: "test" },
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
undefined,
|
||||
2000,
|
||||
);
|
||||
});
|
||||
|
||||
it("receives (adds, updates, then removes when redacted)", async () => {
|
||||
await makeClient({ receiveEvent: [EventType.RTCMembership, EventType.RoomRedaction] });
|
||||
const room = client.getRoom("!1:example.org")!;
|
||||
|
||||
function expectStickyEvents(events: IEvent[]) {
|
||||
expect([...room._unstable_getStickyEvents()].map((e) => e.getEffectiveEvent())).toEqual(events);
|
||||
}
|
||||
|
||||
async function sendAndExpectStickyUpdate(
|
||||
eventToSend: IEvent,
|
||||
added: IEvent[],
|
||||
updated: { current: IEvent; previous: IEvent }[],
|
||||
removed: IEvent[],
|
||||
) {
|
||||
const emittedStickyUpdate = new Promise<
|
||||
Parameters<RoomStickyEventsMap[RoomStickyEventsEvent.Update]>
|
||||
>((resolve) => room.once(RoomStickyEventsEvent.Update, (...args) => resolve(args)));
|
||||
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
|
||||
detail: { data: eventToSend },
|
||||
}),
|
||||
);
|
||||
|
||||
const [addedReceived, updatedReceived, removedReceived] = await emittedStickyUpdate;
|
||||
expect(addedReceived.map((e) => e.getEffectiveEvent())).toEqual(added);
|
||||
expect(
|
||||
updatedReceived.map(({ current, previous }) => ({
|
||||
current: current.getEffectiveEvent(),
|
||||
previous: previous.getEffectiveEvent(),
|
||||
})),
|
||||
).toEqual(updated);
|
||||
expect(removedReceived.map((e) => e.getEffectiveEvent())).toEqual(removed);
|
||||
}
|
||||
|
||||
// First, add a new sticky event to the map. The client should emit.
|
||||
const event1 = new MatrixEvent({
|
||||
type: EventType.RTCMembership,
|
||||
event_id: "$pduhfiidph",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
msc4354_sticky: { duration_ms: 1200000 },
|
||||
content: { msc4354_sticky_key: "test" },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(event1, [event1], [], []);
|
||||
// It should remain cached in the sticky map
|
||||
expectStickyEvents([event1]);
|
||||
|
||||
// Next, update the same key in the sticky map
|
||||
const event2 = new MatrixEvent({
|
||||
type: EventType.RTCMembership,
|
||||
event_id: "$zshgyutptfh",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
msc4354_sticky: { duration_ms: 1200000 },
|
||||
content: { msc4354_sticky_key: "test" },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(event2, [], [{ current: event2, previous: event1 }], []);
|
||||
expectStickyEvents([event2]);
|
||||
|
||||
// Next, redact the second event. Because it has the first as a predecessor, the map should revert to
|
||||
// the first event.
|
||||
const redaction1 = new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
event_id: "$cimoexnvz",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
redacts: event2.event_id,
|
||||
content: { redacts: event2.event_id },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(redaction1, [], [{ current: event1, previous: event2 }], []);
|
||||
expectStickyEvents([event1]);
|
||||
|
||||
// Finally, redact the first event. Now everything should be gone from the map.
|
||||
const redaction2 = new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
event_id: "$drgzmenlh",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
redacts: event1.event_id,
|
||||
content: { redacts: event1.event_id },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(redaction2, [], [], [event1]);
|
||||
expectStickyEvents([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsupported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("fails to send", async () => {
|
||||
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
|
||||
await expect(
|
||||
client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
|
||||
msc4354_sticky_key: "test",
|
||||
}),
|
||||
).rejects.toThrow("Server does not support");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
const unencryptedContentMap = new Map([
|
||||
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
|
||||
|
||||
+52
-5
@@ -38,6 +38,7 @@ import {
|
||||
type SendDelayedEventRequestOpts,
|
||||
type SendDelayedEventResponse,
|
||||
UpdateDelayedEventAction,
|
||||
isSendDelayedEventRequestOpts,
|
||||
} from "./@types/requests.ts";
|
||||
import { EventType, type StateEvents } from "./@types/event.ts";
|
||||
import { logger } from "./logger.ts";
|
||||
@@ -56,7 +57,7 @@ import { ConnectionError, MatrixError } from "./http-api/errors.ts";
|
||||
import { User } from "./models/user.ts";
|
||||
import { type Room } from "./models/room.ts";
|
||||
import { type ToDeviceBatch, type ToDevicePayload } from "./models/ToDeviceMessage.ts";
|
||||
import { MapWithDefault, recursiveMapToObject } from "./utils.ts";
|
||||
import { MapWithDefault, type QueryDict, recursiveMapToObject } from "./utils.ts";
|
||||
import { type EmptyObject, TypedEventEmitter, UnsupportedDelayedEventsEndpointError } from "./matrix.ts";
|
||||
|
||||
interface IStateEventRequest {
|
||||
@@ -122,6 +123,20 @@ export interface ICapabilities {
|
||||
* @defaultValue false
|
||||
*/
|
||||
updateDelayedEvents?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to send sticky events.
|
||||
* @experimental Part of MSC4354 & MSC4407
|
||||
* @defaultValue false
|
||||
*/
|
||||
sendSticky?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to receive sticky events.
|
||||
* @experimental Part of MSC4354 & MSC4407
|
||||
* @defaultValue false
|
||||
*/
|
||||
receiveSticky?: boolean;
|
||||
}
|
||||
|
||||
export enum RoomWidgetClientEvent {
|
||||
@@ -242,6 +257,12 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
if (capabilities.updateDelayedEvents) {
|
||||
widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
}
|
||||
if (capabilities.sendSticky) {
|
||||
widgetApi.requestCapability(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
}
|
||||
if (capabilities.receiveSticky) {
|
||||
widgetApi.requestCapability(MatrixCapabilities.MSC4407ReceiveStickyEvent);
|
||||
}
|
||||
if (capabilities.turnServers) {
|
||||
widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
|
||||
}
|
||||
@@ -346,17 +367,41 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
throw new Error(`Unknown room: ${roomIdOrAlias}`);
|
||||
}
|
||||
|
||||
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse>;
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse>;
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
delayOpts: SendDelayedEventRequestOpts,
|
||||
): Promise<SendDelayedEventResponse>;
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse>;
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
delayOpts?: SendDelayedEventRequestOpts,
|
||||
delayOptsOrQuery?: SendDelayedEventRequestOpts | QueryDict,
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse | SendDelayedEventResponse> {
|
||||
let queryOpts = queryDict;
|
||||
let delayOpts: SendDelayedEventRequestOpts | undefined;
|
||||
if (delayOptsOrQuery && isSendDelayedEventRequestOpts(delayOptsOrQuery)) {
|
||||
delayOpts = delayOptsOrQuery;
|
||||
} else if (!queryOpts) {
|
||||
queryOpts = delayOptsOrQuery;
|
||||
}
|
||||
|
||||
const stickyDurationMs = queryOpts?.["org.matrix.msc4354.sticky_duration_ms"];
|
||||
if (stickyDurationMs !== undefined && typeof stickyDurationMs !== "number") {
|
||||
throw new Error("Sticky duration must be a number when defined");
|
||||
}
|
||||
// This is save since we just checked that above
|
||||
// We need the additional as assertion for the EW linter to be happy.
|
||||
// It is not capable of implying the type based on the throw if `stickyDurationMs !== undefined && typeof stickyDurationMs !== "number"`
|
||||
// above
|
||||
const stickyDurationMsAsNumber: number | undefined = stickyDurationMs as number | undefined;
|
||||
|
||||
// We need to extend the content with the redacts parameter
|
||||
// The js sdk uses event.redacts but the widget api uses event.content.redacts
|
||||
// This will be converted back to event.redacts in the widget driver.
|
||||
@@ -374,6 +419,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
room.roomId,
|
||||
"delay" in delayOpts ? delayOpts.delay : undefined,
|
||||
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
|
||||
stickyDurationMsAsNumber,
|
||||
)
|
||||
.catch(timeoutToConnectionError);
|
||||
return this.validateSendDelayedEventResponse(response);
|
||||
@@ -386,7 +432,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
let response: ISendEventFromWidgetResponseData;
|
||||
try {
|
||||
response = await this.widgetApi
|
||||
.sendRoomEvent(event.getType(), content, room.roomId)
|
||||
.sendRoomEvent(event.getType(), content, room.roomId, undefined, undefined, stickyDurationMsAsNumber)
|
||||
.catch(timeoutToConnectionError);
|
||||
} catch (e) {
|
||||
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
|
||||
@@ -704,6 +750,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
}
|
||||
|
||||
this.emit(ClientEvent.Event, event);
|
||||
if (event.unstableStickyInfo !== undefined) this.room!._unstable_addStickyEvents([event]);
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
logger.info(`Received event ${event.getId()} ${event.getType()}`);
|
||||
} else {
|
||||
|
||||
@@ -4416,10 +4416,10 @@ matrix-mock-request@^2.5.0:
|
||||
dependencies:
|
||||
expect "^28.1.0"
|
||||
|
||||
matrix-widget-api@^1.14.0:
|
||||
version "1.16.0"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.16.0.tgz#e232f1ed6b840feea58d693d877fb8a05b181aee"
|
||||
integrity sha512-OCsCzEN54jWamvWkBa7PqcKdlOhLA+nJbUyqsATHvzb4/NMcjdUZWSDurZxyNE5eYlNwxClA6Hw20mzJEKJbvg==
|
||||
matrix-widget-api@^1.16.1:
|
||||
version "1.16.1"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.16.1.tgz#a447f28f0af07e1bdc960881971de7d1ec9e6464"
|
||||
integrity sha512-oCfTV4xNPo02qIgveqdkIyKQjOPpsjhF3bmJBotHrhr8TsrhVa7kx8PtuiUPnQTjz0tdBle7falR2Fw8VKsedw==
|
||||
dependencies:
|
||||
"@types/events" "^3.0.0"
|
||||
events "^3.2.0"
|
||||
|
||||
Reference in New Issue
Block a user