Skip to content

Commit 977d032

Browse files
authored
Add parseCallNotificationContent (#5015)
* add parseCallNotificationContent Signed-off-by: Timo K <[email protected]> * add tests Signed-off-by: Timo K <[email protected]> * remove decline reason and better m.mentions check Signed-off-by: Timo K <[email protected]> * cap ring duration to EX value (90s) Signed-off-by: Timo K <[email protected]> --------- Signed-off-by: Timo K <[email protected]>
1 parent dd7394c commit 977d032

File tree

2 files changed

+174
-3
lines changed

2 files changed

+174
-3
lines changed

spec/unit/matrixrtc/types.spec.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
Copyright 2025 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { type CallMembership } from "../../../src/matrixrtc";
18+
import { isMyMembership, parseCallNotificationContent } from "../../../src/matrixrtc/types";
19+
20+
describe("types", () => {
21+
describe("isMyMembership", () => {
22+
it("returns false if userId is different", () => {
23+
expect(
24+
isMyMembership(
25+
{ sender: "@alice:example.org", deviceId: "DEVICE" } as CallMembership,
26+
"@bob:example.org",
27+
"DEVICE",
28+
),
29+
).toBe(false);
30+
});
31+
it("returns true if userId and device is the same", () => {
32+
expect(
33+
isMyMembership(
34+
{ sender: "@alice:example.org", deviceId: "DEVICE" } as CallMembership,
35+
"@alice:example.org",
36+
"DEVICE",
37+
),
38+
).toBe(true);
39+
});
40+
});
41+
});
42+
43+
describe("IRTCNotificationContent", () => {
44+
const validBase = Object.freeze({
45+
"m.mentions": { user_ids: [], room: true },
46+
"notification_type": "notification",
47+
"sender_ts": 123,
48+
"lifetime": 1000,
49+
});
50+
51+
it("parses valid content", () => {
52+
const res = parseCallNotificationContent({ ...validBase });
53+
expect(res).toMatchObject(validBase);
54+
});
55+
56+
it("caps lifetime to 90000ms", () => {
57+
const res = parseCallNotificationContent({ ...validBase, lifetime: 130000 });
58+
expect(res.lifetime).toBe(90000);
59+
});
60+
61+
it("throws on malformed m.mentions", () => {
62+
expect(() =>
63+
parseCallNotificationContent({
64+
...validBase,
65+
"m.mentions": "not an object",
66+
} as any),
67+
).toThrow("malformed m.mentions");
68+
});
69+
70+
it("throws on missing or invalid notification_type", () => {
71+
expect(() =>
72+
parseCallNotificationContent({
73+
...validBase,
74+
notification_type: undefined,
75+
} as any),
76+
).toThrow("Missing or invalid notification_type");
77+
78+
expect(() =>
79+
parseCallNotificationContent({
80+
...validBase,
81+
notification_type: 123 as any,
82+
} as any),
83+
).toThrow("Missing or invalid notification_type");
84+
});
85+
86+
it("throws on missing or invalid sender_ts", () => {
87+
expect(() =>
88+
parseCallNotificationContent({
89+
...validBase,
90+
sender_ts: undefined,
91+
} as any),
92+
).toThrow("Missing or invalid sender_ts");
93+
94+
expect(() =>
95+
parseCallNotificationContent({
96+
...validBase,
97+
sender_ts: "123" as any,
98+
} as any),
99+
).toThrow("Missing or invalid sender_ts");
100+
});
101+
102+
it("throws on missing or invalid lifetime", () => {
103+
expect(() =>
104+
parseCallNotificationContent({
105+
...validBase,
106+
lifetime: undefined,
107+
} as any),
108+
).toThrow("Missing or invalid lifetime");
109+
110+
expect(() =>
111+
parseCallNotificationContent({
112+
...validBase,
113+
lifetime: "1000" as any,
114+
} as any),
115+
).toThrow("Missing or invalid lifetime");
116+
});
117+
118+
it("accepts valid relation (m.reference)", () => {
119+
// Note: parseCallNotificationContent currently checks `relation.rel_type` rather than `m.relates_to`.
120+
const res = parseCallNotificationContent({
121+
...validBase,
122+
relation: { rel_type: "m.reference", event_id: "$ev" },
123+
} as any);
124+
expect(res).toBeTruthy();
125+
});
126+
127+
it("throws on invalid relation rel_type", () => {
128+
expect(() =>
129+
parseCallNotificationContent({
130+
...validBase,
131+
relation: { rel_type: "m.annotation", event_id: "$ev" },
132+
} as any),
133+
).toThrow("Invalid relation");
134+
});
135+
});

src/matrixrtc/types.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
16-
import type { IMentions } from "../matrix.ts";
16+
import type { IContent, IMentions } from "../matrix.ts";
1717
import type { RelationEvent } from "../types.ts";
1818
import type { CallMembership } from "./CallMembership.ts";
1919

@@ -102,9 +102,45 @@ export type RTCNotificationType = "ring" | "notification";
102102
* May be any string, although `"audio"` and `"video"` are commonly accepted values.
103103
*/
104104
export type RTCCallIntent = "audio" | "video" | string;
105+
106+
/**
107+
* This will check if the content has all the expected fields to be a valid IRTCNotificationContent.
108+
* It will also cap the lifetime to 90000ms (1.5 min) if a higher value is provided.
109+
* @param content
110+
* @throws if the content is invalid
111+
* @returns a parsed IRTCNotificationContent
112+
*/
113+
export function parseCallNotificationContent(content: IContent): IRTCNotificationContent {
114+
if (content["m.mentions"] && typeof content["m.mentions"] !== "object") {
115+
throw new Error("malformed m.mentions");
116+
}
117+
if (typeof content["notification_type"] !== "string") {
118+
throw new Error("Missing or invalid notification_type");
119+
}
120+
if (typeof content["sender_ts"] !== "number") {
121+
throw new Error("Missing or invalid sender_ts");
122+
}
123+
if (typeof content["lifetime"] !== "number") {
124+
throw new Error("Missing or invalid lifetime");
125+
}
126+
127+
if (content["relation"] && content["relation"]["rel_type"] !== "m.reference") {
128+
throw new Error("Invalid relation");
129+
}
130+
if (content["m.call.intent"] && typeof content["m.call.intent"] !== "string") {
131+
throw new Error("Invalid m.call.intent");
132+
}
133+
134+
const cappedLifetime = content["lifetime"] >= 90000 ? 90000 : content["lifetime"];
135+
return { ...content, lifetime: cappedLifetime } as IRTCNotificationContent;
136+
}
137+
138+
/**
139+
* Interface for `org.matrix.msc4075.rtc.notification` events.
140+
* Don't cast event content to this directly. Use `parseCallNotificationContent` instead to validate the content first.
141+
*/
105142
export interface IRTCNotificationContent extends RelationEvent {
106-
"m.mentions": IMentions;
107-
"decline_reason"?: string;
143+
"m.mentions"?: IMentions;
108144
"notification_type": RTCNotificationType;
109145
/**
110146
* The initial intent of the calling user.

0 commit comments

Comments
 (0)