Skip to content

Commit 687d08d

Browse files
authored
Support MSC4140: Delayed events (#4294)
and use them for more reliable MatrixRTC session membership events. Also implement "parent" delayed events, which were in a previous version of the MSC and may be reintroduced or be part of a new MSC later. NOTE: Still missing is support for sending encrypted delayed events.
1 parent 0300d63 commit 687d08d

File tree

6 files changed

+715
-58
lines changed

6 files changed

+715
-58
lines changed

spec/unit/matrix-client.spec.ts

Lines changed: 325 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
Room,
5858
RuleId,
5959
TweakName,
60+
UpdateDelayedEventAction,
6061
} from "../../src";
6162
import { supportsMatrixCall } from "../../src/webrtc/call";
6263
import { makeBeaconEvent } from "../test-utils/beacon";
@@ -97,7 +98,7 @@ type HttpLookup = {
9798
method: string;
9899
path: string;
99100
prefix?: string;
100-
data?: Record<string, any>;
101+
data?: Record<string, any> | Record<string, any>[];
101102
error?: object;
102103
expectBody?: Record<string, any>;
103104
expectQueryParams?: QueryDict;
@@ -704,6 +705,328 @@ describe("MatrixClient", function () {
704705
});
705706
});
706707

708+
describe("_unstable_sendDelayedEvent", () => {
709+
const unstableMSC4140Prefix = `${ClientPrefix.Unstable}/org.matrix.msc4140`;
710+
711+
const roomId = "!room:example.org";
712+
const body = "This is the body";
713+
const content = { body, msgtype: MsgType.Text } satisfies RoomMessageEventContent;
714+
const timeoutDelayOpts = { delay: 2000 };
715+
const realTimeoutDelayOpts = { "org.matrix.msc4140.delay": 2000 };
716+
717+
beforeEach(() => {
718+
unstableFeatures["org.matrix.msc4140"] = true;
719+
});
720+
721+
it("throws when unsupported by server", async () => {
722+
unstableFeatures["org.matrix.msc4140"] = false;
723+
const errorMessage = "Server does not support";
724+
725+
await expect(
726+
client._unstable_sendDelayedEvent(
727+
roomId,
728+
timeoutDelayOpts,
729+
null,
730+
EventType.RoomMessage,
731+
{ ...content },
732+
client.makeTxnId(),
733+
),
734+
).rejects.toThrow(errorMessage);
735+
736+
await expect(
737+
client._unstable_sendDelayedStateEvent(roomId, timeoutDelayOpts, EventType.RoomTopic, {
738+
topic: "topic",
739+
}),
740+
).rejects.toThrow(errorMessage);
741+
742+
await expect(client._unstable_getDelayedEvents()).rejects.toThrow(errorMessage);
743+
744+
await expect(
745+
client._unstable_updateDelayedEvent("anyDelayId", UpdateDelayedEventAction.Send),
746+
).rejects.toThrow(errorMessage);
747+
});
748+
749+
it("works with null threadId", async () => {
750+
httpLookups = [];
751+
752+
const timeoutDelayTxnId = client.makeTxnId();
753+
httpLookups.push({
754+
method: "PUT",
755+
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
756+
expectQueryParams: realTimeoutDelayOpts,
757+
data: { delay_id: "id1" },
758+
expectBody: content,
759+
});
760+
761+
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
762+
roomId,
763+
timeoutDelayOpts,
764+
null,
765+
EventType.RoomMessage,
766+
{ ...content },
767+
timeoutDelayTxnId,
768+
);
769+
770+
const actionDelayTxnId = client.makeTxnId();
771+
httpLookups.push({
772+
method: "PUT",
773+
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
774+
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
775+
data: { delay_id: "id2" },
776+
expectBody: content,
777+
});
778+
779+
await client._unstable_sendDelayedEvent(
780+
roomId,
781+
{ parent_delay_id: timeoutDelayId },
782+
null,
783+
EventType.RoomMessage,
784+
{ ...content },
785+
actionDelayTxnId,
786+
);
787+
});
788+
789+
it("works with non-null threadId", async () => {
790+
httpLookups = [];
791+
const threadId = "$threadId:server";
792+
const expectBody = {
793+
...content,
794+
"m.relates_to": {
795+
event_id: threadId,
796+
is_falling_back: true,
797+
rel_type: "m.thread",
798+
},
799+
};
800+
801+
const timeoutDelayTxnId = client.makeTxnId();
802+
httpLookups.push({
803+
method: "PUT",
804+
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
805+
expectQueryParams: realTimeoutDelayOpts,
806+
data: { delay_id: "id1" },
807+
expectBody,
808+
});
809+
810+
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
811+
roomId,
812+
timeoutDelayOpts,
813+
threadId,
814+
EventType.RoomMessage,
815+
{ ...content },
816+
timeoutDelayTxnId,
817+
);
818+
819+
const actionDelayTxnId = client.makeTxnId();
820+
httpLookups.push({
821+
method: "PUT",
822+
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
823+
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
824+
data: { delay_id: "id2" },
825+
expectBody,
826+
});
827+
828+
await client._unstable_sendDelayedEvent(
829+
roomId,
830+
{ parent_delay_id: timeoutDelayId },
831+
threadId,
832+
EventType.RoomMessage,
833+
{ ...content },
834+
actionDelayTxnId,
835+
);
836+
});
837+
838+
it("should add thread relation if threadId is passed and the relation is missing", async () => {
839+
httpLookups = [];
840+
const threadId = "$threadId:server";
841+
const expectBody = {
842+
...content,
843+
"m.relates_to": {
844+
"m.in_reply_to": {
845+
event_id: threadId,
846+
},
847+
"event_id": threadId,
848+
"is_falling_back": true,
849+
"rel_type": "m.thread",
850+
},
851+
};
852+
853+
const room = new Room(roomId, client, userId);
854+
mocked(store.getRoom).mockReturnValue(room);
855+
856+
const rootEvent = new MatrixEvent({ event_id: threadId });
857+
room.createThread(threadId, rootEvent, [rootEvent], false);
858+
859+
const timeoutDelayTxnId = client.makeTxnId();
860+
httpLookups.push({
861+
method: "PUT",
862+
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
863+
expectQueryParams: realTimeoutDelayOpts,
864+
data: { delay_id: "id1" },
865+
expectBody,
866+
});
867+
868+
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
869+
roomId,
870+
timeoutDelayOpts,
871+
threadId,
872+
EventType.RoomMessage,
873+
{ ...content },
874+
timeoutDelayTxnId,
875+
);
876+
877+
const actionDelayTxnId = client.makeTxnId();
878+
httpLookups.push({
879+
method: "PUT",
880+
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
881+
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
882+
data: { delay_id: "id2" },
883+
expectBody,
884+
});
885+
886+
await client._unstable_sendDelayedEvent(
887+
roomId,
888+
{ parent_delay_id: timeoutDelayId },
889+
threadId,
890+
EventType.RoomMessage,
891+
{ ...content },
892+
actionDelayTxnId,
893+
);
894+
});
895+
896+
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
897+
httpLookups = [];
898+
const threadId = "$threadId:server";
899+
900+
const content = {
901+
body,
902+
"msgtype": MsgType.Text,
903+
"m.relates_to": {
904+
"m.in_reply_to": {
905+
event_id: "$other:event",
906+
},
907+
},
908+
} satisfies RoomMessageEventContent;
909+
const expectBody = {
910+
...content,
911+
"m.relates_to": {
912+
"m.in_reply_to": {
913+
event_id: "$other:event",
914+
},
915+
"event_id": threadId,
916+
"is_falling_back": false,
917+
"rel_type": "m.thread",
918+
},
919+
};
920+
921+
const room = new Room(roomId, client, userId);
922+
mocked(store.getRoom).mockReturnValue(room);
923+
924+
const rootEvent = new MatrixEvent({ event_id: threadId });
925+
room.createThread(threadId, rootEvent, [rootEvent], false);
926+
927+
const timeoutDelayTxnId = client.makeTxnId();
928+
httpLookups.push({
929+
method: "PUT",
930+
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
931+
expectQueryParams: realTimeoutDelayOpts,
932+
data: { delay_id: "id1" },
933+
expectBody,
934+
});
935+
936+
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
937+
roomId,
938+
timeoutDelayOpts,
939+
threadId,
940+
EventType.RoomMessage,
941+
{ ...content },
942+
timeoutDelayTxnId,
943+
);
944+
945+
const actionDelayTxnId = client.makeTxnId();
946+
httpLookups.push({
947+
method: "PUT",
948+
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
949+
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
950+
data: { delay_id: "id2" },
951+
expectBody,
952+
});
953+
954+
await client._unstable_sendDelayedEvent(
955+
roomId,
956+
{ parent_delay_id: timeoutDelayId },
957+
threadId,
958+
EventType.RoomMessage,
959+
{ ...content },
960+
actionDelayTxnId,
961+
);
962+
});
963+
964+
it("can send a delayed state event", async () => {
965+
httpLookups = [];
966+
const content = { topic: "The year 2000" };
967+
968+
httpLookups.push({
969+
method: "PUT",
970+
path: `/rooms/${encodeURIComponent(roomId)}/state/m.room.topic/`,
971+
expectQueryParams: realTimeoutDelayOpts,
972+
data: { delay_id: "id1" },
973+
expectBody: content,
974+
});
975+
976+
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedStateEvent(
977+
roomId,
978+
timeoutDelayOpts,
979+
EventType.RoomTopic,
980+
{ ...content },
981+
);
982+
983+
httpLookups.push({
984+
method: "PUT",
985+
path: `/rooms/${encodeURIComponent(roomId)}/state/m.room.topic/`,
986+
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
987+
data: { delay_id: "id2" },
988+
expectBody: content,
989+
});
990+
991+
await client._unstable_sendDelayedStateEvent(
992+
roomId,
993+
{ parent_delay_id: timeoutDelayId },
994+
EventType.RoomTopic,
995+
{ ...content },
996+
);
997+
});
998+
999+
it("can look up delayed events", async () => {
1000+
httpLookups = [
1001+
{
1002+
method: "GET",
1003+
prefix: unstableMSC4140Prefix,
1004+
path: "/delayed_events",
1005+
data: [],
1006+
},
1007+
];
1008+
1009+
await client._unstable_getDelayedEvents();
1010+
});
1011+
1012+
it("can update delayed events", async () => {
1013+
const delayId = "id";
1014+
const action = UpdateDelayedEventAction.Restart;
1015+
httpLookups = [
1016+
{
1017+
method: "POST",
1018+
prefix: unstableMSC4140Prefix,
1019+
path: `/delayed_events/${encodeURIComponent(delayId)}`,
1020+
data: {
1021+
action,
1022+
},
1023+
},
1024+
];
1025+
1026+
await client._unstable_updateDelayedEvent(delayId, action);
1027+
});
1028+
});
1029+
7071030
it("should create (unstable) file trees", async () => {
7081031
const userId = "@test:example.org";
7091032
const roomId = "!room:example.org";
@@ -963,7 +1286,7 @@ describe("MatrixClient", function () {
9631286
const filter = new Filter(client.credentials.userId);
9641287

9651288
const filterId = await client.getOrCreateFilter(filterName, filter);
966-
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
1289+
expect(filterId).toEqual(!Array.isArray(FILTER_RESPONSE.data) && FILTER_RESPONSE.data?.filter_id);
9671290
});
9681291
});
9691292

0 commit comments

Comments
 (0)