Skip to content

Commit dbb2ae5

Browse files
robintowntoger5
andauthored
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>
1 parent c8032a2 commit dbb2ae5

File tree

4 files changed

+228
-11
lines changed

4 files changed

+228
-11
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"jwt-decode": "^4.0.0",
5757
"loglevel": "^1.9.2",
5858
"matrix-events-sdk": "0.0.1",
59-
"matrix-widget-api": "^1.14.0",
59+
"matrix-widget-api": "^1.16.1",
6060
"oidc-client-ts": "^3.0.1",
6161
"p-retry": "7",
6262
"sdp-transform": "^3.0.0",

spec/unit/embedded.spec.ts

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ import {
3232
type IRoomEvent,
3333
} from "matrix-widget-api";
3434

35-
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
35+
import {
36+
createRoomWidgetClient,
37+
EventType,
38+
type IEvent,
39+
MatrixError,
40+
MsgType,
41+
UpdateDelayedEventAction,
42+
} from "../../src/matrix";
3643
import { MatrixClient, ClientEvent, type ITurnServer as IClientTurnServer } from "../../src/client";
3744
import { SyncState } from "../../src/sync";
3845
import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
@@ -42,13 +49,15 @@ import { sleep } from "../../src/utils";
4249
import { SlidingSync } from "../../src/sliding-sync";
4350
import { logger } from "../../src/logger";
4451
import { flushPromises } from "../test-utils/flushPromises";
52+
import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../../src/models/room-sticky-events";
4553

4654
const testOIDCToken = {
4755
access_token: "12345678",
4856
expires_in: "10",
4957
matrix_server_name: "homeserver.oabc",
5058
token_type: "Bearer",
5159
};
60+
5261
class MockWidgetApi extends EventEmitter {
5362
public start = vi.fn().mockResolvedValue(undefined);
5463
public getClientVersions = vi.fn();
@@ -167,6 +176,9 @@ describe("RoomWidgetClient", () => {
167176
"org.matrix.rageshake_request",
168177
{ request_id: 123 },
169178
"!1:example.org",
179+
undefined,
180+
undefined,
181+
undefined,
170182
);
171183
});
172184

@@ -422,6 +434,7 @@ describe("RoomWidgetClient", () => {
422434
"!1:example.org",
423435
2000,
424436
undefined,
437+
undefined,
425438
);
426439
});
427440

@@ -442,6 +455,7 @@ describe("RoomWidgetClient", () => {
442455
"!1:example.org",
443456
undefined,
444457
parentDelayId,
458+
undefined,
445459
);
446460
});
447461

@@ -855,6 +869,162 @@ describe("RoomWidgetClient", () => {
855869
});
856870
});
857871

872+
describe("sticky events", () => {
873+
describe("when supported", () => {
874+
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
875+
Promise.resolve(feature === "org.matrix.msc4354"),
876+
);
877+
878+
beforeAll(() => {
879+
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
880+
});
881+
882+
afterAll(() => {
883+
doesServerSupportUnstableFeatureMock.mockReset();
884+
});
885+
886+
it("requests capabilities when set", async () => {
887+
await makeClient({ sendSticky: true, receiveSticky: true });
888+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
889+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407ReceiveStickyEvent);
890+
});
891+
892+
it("does not request capabilities when unset", async () => {
893+
await makeClient({});
894+
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
895+
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(
896+
MatrixCapabilities.MSC4407ReceiveStickyEvent,
897+
);
898+
});
899+
900+
it("sends", async () => {
901+
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
902+
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
903+
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith(EventType.RTCMembership);
904+
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
905+
await client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
906+
msc4354_sticky_key: "test",
907+
});
908+
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
909+
EventType.RTCMembership,
910+
{ msc4354_sticky_key: "test" },
911+
"!1:example.org",
912+
undefined,
913+
undefined,
914+
2000,
915+
);
916+
});
917+
918+
it("receives (adds, updates, then removes when redacted)", async () => {
919+
await makeClient({ receiveEvent: [EventType.RTCMembership, EventType.RoomRedaction] });
920+
const room = client.getRoom("!1:example.org")!;
921+
922+
function expectStickyEvents(events: IEvent[]) {
923+
expect([...room._unstable_getStickyEvents()].map((e) => e.getEffectiveEvent())).toEqual(events);
924+
}
925+
926+
async function sendAndExpectStickyUpdate(
927+
eventToSend: IEvent,
928+
added: IEvent[],
929+
updated: { current: IEvent; previous: IEvent }[],
930+
removed: IEvent[],
931+
) {
932+
const emittedStickyUpdate = new Promise<
933+
Parameters<RoomStickyEventsMap[RoomStickyEventsEvent.Update]>
934+
>((resolve) => room.once(RoomStickyEventsEvent.Update, (...args) => resolve(args)));
935+
936+
widgetApi.emit(
937+
`action:${WidgetApiToWidgetAction.SendEvent}`,
938+
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
939+
detail: { data: eventToSend },
940+
}),
941+
);
942+
943+
const [addedReceived, updatedReceived, removedReceived] = await emittedStickyUpdate;
944+
expect(addedReceived.map((e) => e.getEffectiveEvent())).toEqual(added);
945+
expect(
946+
updatedReceived.map(({ current, previous }) => ({
947+
current: current.getEffectiveEvent(),
948+
previous: previous.getEffectiveEvent(),
949+
})),
950+
).toEqual(updated);
951+
expect(removedReceived.map((e) => e.getEffectiveEvent())).toEqual(removed);
952+
}
953+
954+
// First, add a new sticky event to the map. The client should emit.
955+
const event1 = new MatrixEvent({
956+
type: EventType.RTCMembership,
957+
event_id: "$pduhfiidph",
958+
room_id: "!1:example.org",
959+
sender: "@alice:example.org",
960+
msc4354_sticky: { duration_ms: 1200000 },
961+
content: { msc4354_sticky_key: "test" },
962+
}).getEffectiveEvent();
963+
await sendAndExpectStickyUpdate(event1, [event1], [], []);
964+
// It should remain cached in the sticky map
965+
expectStickyEvents([event1]);
966+
967+
// Next, update the same key in the sticky map
968+
const event2 = new MatrixEvent({
969+
type: EventType.RTCMembership,
970+
event_id: "$zshgyutptfh",
971+
room_id: "!1:example.org",
972+
sender: "@alice:example.org",
973+
msc4354_sticky: { duration_ms: 1200000 },
974+
content: { msc4354_sticky_key: "test" },
975+
}).getEffectiveEvent();
976+
await sendAndExpectStickyUpdate(event2, [], [{ current: event2, previous: event1 }], []);
977+
expectStickyEvents([event2]);
978+
979+
// Next, redact the second event. Because it has the first as a predecessor, the map should revert to
980+
// the first event.
981+
const redaction1 = new MatrixEvent({
982+
type: EventType.RoomRedaction,
983+
event_id: "$cimoexnvz",
984+
room_id: "!1:example.org",
985+
sender: "@alice:example.org",
986+
redacts: event2.event_id,
987+
content: { redacts: event2.event_id },
988+
}).getEffectiveEvent();
989+
await sendAndExpectStickyUpdate(redaction1, [], [{ current: event1, previous: event2 }], []);
990+
expectStickyEvents([event1]);
991+
992+
// Finally, redact the first event. Now everything should be gone from the map.
993+
const redaction2 = new MatrixEvent({
994+
type: EventType.RoomRedaction,
995+
event_id: "$drgzmenlh",
996+
room_id: "!1:example.org",
997+
sender: "@alice:example.org",
998+
redacts: event1.event_id,
999+
content: { redacts: event1.event_id },
1000+
}).getEffectiveEvent();
1001+
await sendAndExpectStickyUpdate(redaction2, [], [], [event1]);
1002+
expectStickyEvents([]);
1003+
});
1004+
});
1005+
1006+
describe("when unsupported", () => {
1007+
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
1008+
1009+
beforeAll(() => {
1010+
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
1011+
});
1012+
1013+
afterAll(() => {
1014+
doesServerSupportUnstableFeatureMock.mockReset();
1015+
});
1016+
1017+
it("fails to send", async () => {
1018+
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
1019+
await expect(
1020+
client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
1021+
msc4354_sticky_key: "test",
1022+
}),
1023+
).rejects.toThrow("Server does not support");
1024+
});
1025+
});
1026+
});
1027+
8581028
describe("to-device messages", () => {
8591029
const unencryptedContentMap = new Map([
8601030
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],

src/embedded.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
type SendDelayedEventRequestOpts,
3939
type SendDelayedEventResponse,
4040
UpdateDelayedEventAction,
41+
isSendDelayedEventRequestOpts,
4142
} from "./@types/requests.ts";
4243
import { EventType, type StateEvents } from "./@types/event.ts";
4344
import { logger } from "./logger.ts";
@@ -56,7 +57,7 @@ import { ConnectionError, MatrixError } from "./http-api/errors.ts";
5657
import { User } from "./models/user.ts";
5758
import { type Room } from "./models/room.ts";
5859
import { type ToDeviceBatch, type ToDevicePayload } from "./models/ToDeviceMessage.ts";
59-
import { MapWithDefault, recursiveMapToObject } from "./utils.ts";
60+
import { MapWithDefault, type QueryDict, recursiveMapToObject } from "./utils.ts";
6061
import { type EmptyObject, TypedEventEmitter, UnsupportedDelayedEventsEndpointError } from "./matrix.ts";
6162

6263
interface IStateEventRequest {
@@ -122,6 +123,20 @@ export interface ICapabilities {
122123
* @defaultValue false
123124
*/
124125
updateDelayedEvents?: boolean;
126+
127+
/**
128+
* Whether this client needs to be able to send sticky events.
129+
* @experimental Part of MSC4354 & MSC4407
130+
* @defaultValue false
131+
*/
132+
sendSticky?: boolean;
133+
134+
/**
135+
* Whether this client needs to be able to receive sticky events.
136+
* @experimental Part of MSC4354 & MSC4407
137+
* @defaultValue false
138+
*/
139+
receiveSticky?: boolean;
125140
}
126141

127142
export enum RoomWidgetClientEvent {
@@ -242,6 +257,12 @@ export class RoomWidgetClient extends MatrixClient {
242257
if (capabilities.updateDelayedEvents) {
243258
widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
244259
}
260+
if (capabilities.sendSticky) {
261+
widgetApi.requestCapability(MatrixCapabilities.MSC4407SendStickyEvent);
262+
}
263+
if (capabilities.receiveSticky) {
264+
widgetApi.requestCapability(MatrixCapabilities.MSC4407ReceiveStickyEvent);
265+
}
245266
if (capabilities.turnServers) {
246267
widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
247268
}
@@ -346,17 +367,41 @@ export class RoomWidgetClient extends MatrixClient {
346367
throw new Error(`Unknown room: ${roomIdOrAlias}`);
347368
}
348369

349-
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse>;
370+
protected async encryptAndSendEvent(
371+
room: Room,
372+
event: MatrixEvent,
373+
queryDict?: QueryDict,
374+
): Promise<ISendEventResponse>;
350375
protected async encryptAndSendEvent(
351376
room: Room,
352377
event: MatrixEvent,
353378
delayOpts: SendDelayedEventRequestOpts,
354-
): Promise<SendDelayedEventResponse>;
379+
queryDict?: QueryDict,
380+
): Promise<ISendEventResponse>;
355381
protected async encryptAndSendEvent(
356382
room: Room,
357383
event: MatrixEvent,
358-
delayOpts?: SendDelayedEventRequestOpts,
384+
delayOptsOrQuery?: SendDelayedEventRequestOpts | QueryDict,
385+
queryDict?: QueryDict,
359386
): Promise<ISendEventResponse | SendDelayedEventResponse> {
387+
let queryOpts = queryDict;
388+
let delayOpts: SendDelayedEventRequestOpts | undefined;
389+
if (delayOptsOrQuery && isSendDelayedEventRequestOpts(delayOptsOrQuery)) {
390+
delayOpts = delayOptsOrQuery;
391+
} else if (!queryOpts) {
392+
queryOpts = delayOptsOrQuery;
393+
}
394+
395+
const stickyDurationMs = queryOpts?.["org.matrix.msc4354.sticky_duration_ms"];
396+
if (stickyDurationMs !== undefined && typeof stickyDurationMs !== "number") {
397+
throw new Error("Sticky duration must be a number when defined");
398+
}
399+
// This is save since we just checked that above
400+
// We need the additional as assertion for the EW linter to be happy.
401+
// It is not capable of implying the type based on the throw if `stickyDurationMs !== undefined && typeof stickyDurationMs !== "number"`
402+
// above
403+
const stickyDurationMsAsNumber: number | undefined = stickyDurationMs as number | undefined;
404+
360405
// We need to extend the content with the redacts parameter
361406
// The js sdk uses event.redacts but the widget api uses event.content.redacts
362407
// This will be converted back to event.redacts in the widget driver.
@@ -374,6 +419,7 @@ export class RoomWidgetClient extends MatrixClient {
374419
room.roomId,
375420
"delay" in delayOpts ? delayOpts.delay : undefined,
376421
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
422+
stickyDurationMsAsNumber,
377423
)
378424
.catch(timeoutToConnectionError);
379425
return this.validateSendDelayedEventResponse(response);
@@ -386,7 +432,7 @@ export class RoomWidgetClient extends MatrixClient {
386432
let response: ISendEventFromWidgetResponseData;
387433
try {
388434
response = await this.widgetApi
389-
.sendRoomEvent(event.getType(), content, room.roomId)
435+
.sendRoomEvent(event.getType(), content, room.roomId, undefined, undefined, stickyDurationMsAsNumber)
390436
.catch(timeoutToConnectionError);
391437
} catch (e) {
392438
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
@@ -704,6 +750,7 @@ export class RoomWidgetClient extends MatrixClient {
704750
}
705751

706752
this.emit(ClientEvent.Event, event);
753+
if (event.unstableStickyInfo !== undefined) this.room!._unstable_addStickyEvents([event]);
707754
this.setSyncState(SyncState.Syncing);
708755
logger.info(`Received event ${event.getId()} ${event.getType()}`);
709756
} else {

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4416,10 +4416,10 @@ matrix-mock-request@^2.5.0:
44164416
dependencies:
44174417
expect "^28.1.0"
44184418

4419-
matrix-widget-api@^1.14.0:
4420-
version "1.16.0"
4421-
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.16.0.tgz#e232f1ed6b840feea58d693d877fb8a05b181aee"
4422-
integrity sha512-OCsCzEN54jWamvWkBa7PqcKdlOhLA+nJbUyqsATHvzb4/NMcjdUZWSDurZxyNE5eYlNwxClA6Hw20mzJEKJbvg==
4419+
matrix-widget-api@^1.16.1:
4420+
version "1.16.1"
4421+
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.16.1.tgz#a447f28f0af07e1bdc960881971de7d1ec9e6464"
4422+
integrity sha512-oCfTV4xNPo02qIgveqdkIyKQjOPpsjhF3bmJBotHrhr8TsrhVa7kx8PtuiUPnQTjz0tdBle7falR2Fw8VKsedw==
44234423
dependencies:
44244424
"@types/events" "^3.0.0"
44254425
events "^3.2.0"

0 commit comments

Comments
 (0)