From 5358215501d4789d0b478af1707e51eff184f20f Mon Sep 17 00:00:00 2001 From: Timo K Date: Fri, 19 Sep 2025 11:09:37 +0200 Subject: [PATCH 1/3] add parseCallNotificationContent Signed-off-by: Timo K --- src/matrixrtc/types.ts | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/matrixrtc/types.ts b/src/matrixrtc/types.ts index b344a22d8b..ebce34d427 100644 --- a/src/matrixrtc/types.ts +++ b/src/matrixrtc/types.ts @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import type { IMentions } from "../matrix.ts"; +import type { IContent, IMentions } from "../matrix.ts"; import type { RelationEvent } from "../types.ts"; import type { CallMembership } from "./CallMembership.ts"; @@ -102,6 +102,46 @@ export type RTCNotificationType = "ring" | "notification"; * May be any string, although `"audio"` and `"video"` are commonly accepted values. */ export type RTCCallIntent = "audio" | "video" | string; + +/** + * This will check if the content has all the expected fields to be a valid IRTCNotificationContent. + * It will also cap the lifetime to 120000ms if a higher value is provided. + * @param content + * @throws if the content is invalid + * @returns a parsed IRTCNotificationContent + */ +export function parseCallNotificationContent(content: IContent): IRTCNotificationContent { + if (typeof content["m.mentions"] !== "object") { + throw new Error("Missing m.mentions"); + } + if (typeof content["notification_type"] !== "string") { + throw new Error("Missing or invalid notification_type"); + } + if (typeof content["sender_ts"] !== "number") { + throw new Error("Missing or invalid sender_ts"); + } + if (typeof content["lifetime"] !== "number") { + throw new Error("Missing or invalid lifetime"); + } + + if (content["decline_reason"] && typeof content["decline_reason"] !== "string") { + throw new Error("Invalid decline_reason"); + } + if (content["relation"] && content["relation"]["rel_type"] !== "m.reference") { + throw new Error("Invalid relation"); + } + if (content["m.call.intent"] && typeof content["m.call.intent"] !== "string") { + throw new Error("Invalid m.call.intent"); + } + + const cappedLifetime = content["lifetime"] >= 120000 ? 120000 : content["lifetime"]; + return { lifetime: cappedLifetime, ...content } as IRTCNotificationContent; +} + +/** + * Interface for `org.matrix.msc4075.rtc.notification` events. + * Don't cast event content to this directly. Use `parseCallNotificationContent` instead to validate the content first. + */ export interface IRTCNotificationContent extends RelationEvent { "m.mentions": IMentions; "decline_reason"?: string; From 604b0f204ba0c4f07d82579037846c8331f3d880 Mon Sep 17 00:00:00 2001 From: Timo K Date: Fri, 19 Sep 2025 11:38:04 +0200 Subject: [PATCH 2/3] add tests Signed-off-by: Timo K --- spec/unit/matrixrtc/types.spec.ts | 145 ++++++++++++++++++++++++++++++ src/matrixrtc/types.ts | 2 +- 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 spec/unit/matrixrtc/types.spec.ts diff --git a/spec/unit/matrixrtc/types.spec.ts b/spec/unit/matrixrtc/types.spec.ts new file mode 100644 index 0000000000..179d16c6b1 --- /dev/null +++ b/spec/unit/matrixrtc/types.spec.ts @@ -0,0 +1,145 @@ +/* +Copyright 2025 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { type CallMembership } from "../../../src/matrixrtc"; +import { isMyMembership, parseCallNotificationContent } from "../../../src/matrixrtc/types"; + +describe("types", () => { + describe("isMyMembership", () => { + it("returns false if userId is different", () => { + expect( + isMyMembership( + { sender: "@alice:example.org", deviceId: "DEVICE" } as CallMembership, + "@bob:example.org", + "DEVICE", + ), + ).toBe(false); + }); + it("returns true if userId and device is the same", () => { + expect( + isMyMembership( + { sender: "@alice:example.org", deviceId: "DEVICE" } as CallMembership, + "@alice:example.org", + "DEVICE", + ), + ).toBe(true); + }); + }); +}); + +describe("IRTCNotificationContent", () => { + const validBase = Object.freeze({ + "m.mentions": { user_ids: [], room: true }, + "notification_type": "notification", + "sender_ts": 123, + "lifetime": 1000, + }); + + it("parses valid content", () => { + const res = parseCallNotificationContent({ ...validBase }); + expect(res).toMatchObject(validBase); + }); + + it("caps lifetime to 120000ms", () => { + const res = parseCallNotificationContent({ ...validBase, lifetime: 130000 }); + expect(res.lifetime).toBe(120000); + }); + + it("throws on missing m.mentions", () => { + expect(() => + parseCallNotificationContent({ + notification_type: "notification", + sender_ts: 123, + lifetime: 1000, + } as any), + ).toThrow("Missing m.mentions"); + }); + + it("throws on missing or invalid notification_type", () => { + expect(() => + parseCallNotificationContent({ + ...validBase, + notification_type: undefined, + } as any), + ).toThrow("Missing or invalid notification_type"); + + expect(() => + parseCallNotificationContent({ + ...validBase, + notification_type: 123 as any, + } as any), + ).toThrow("Missing or invalid notification_type"); + }); + + it("throws on missing or invalid sender_ts", () => { + expect(() => + parseCallNotificationContent({ + ...validBase, + sender_ts: undefined, + } as any), + ).toThrow("Missing or invalid sender_ts"); + + expect(() => + parseCallNotificationContent({ + ...validBase, + sender_ts: "123" as any, + } as any), + ).toThrow("Missing or invalid sender_ts"); + }); + + it("throws on missing or invalid lifetime", () => { + expect(() => + parseCallNotificationContent({ + ...validBase, + lifetime: undefined, + } as any), + ).toThrow("Missing or invalid lifetime"); + + expect(() => + parseCallNotificationContent({ + ...validBase, + lifetime: "1000" as any, + } as any), + ).toThrow("Missing or invalid lifetime"); + }); + + it("throws on invalid decline_reason type", () => { + expect(() => + parseCallNotificationContent({ + ...validBase, + decline_reason: 42 as any, + } as any), + ).toThrow("Invalid decline_reason"); + }); + + it("accepts valid relation (m.reference)", () => { + // Note: parseCallNotificationContent currently checks `relation.rel_type` rather than `m.relates_to`. + const res = parseCallNotificationContent({ + ...validBase, + relation: { rel_type: "m.reference", event_id: "$ev" }, + } as any); + expect(res).toBeTruthy(); + }); + + it("throws on invalid relation rel_type", () => { + expect(() => + parseCallNotificationContent({ + ...validBase, + relation: { rel_type: "m.annotation", event_id: "$ev" }, + } as any), + ).toThrow("Invalid relation"); + }); +}); diff --git a/src/matrixrtc/types.ts b/src/matrixrtc/types.ts index ebce34d427..4f4d444f5c 100644 --- a/src/matrixrtc/types.ts +++ b/src/matrixrtc/types.ts @@ -135,7 +135,7 @@ export function parseCallNotificationContent(content: IContent): IRTCNotificatio } const cappedLifetime = content["lifetime"] >= 120000 ? 120000 : content["lifetime"]; - return { lifetime: cappedLifetime, ...content } as IRTCNotificationContent; + return { ...content, lifetime: cappedLifetime } as IRTCNotificationContent; } /** From 6cf387842e51e3ca788e91a2c450e7b87f39e722 Mon Sep 17 00:00:00 2001 From: Timo K Date: Fri, 19 Sep 2025 16:41:54 +0200 Subject: [PATCH 3/3] remove decline reason and better m.mentions check Signed-off-by: Timo K --- spec/unit/matrixrtc/types.spec.ts | 18 ++++-------------- src/matrixrtc/types.ts | 10 +++------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/spec/unit/matrixrtc/types.spec.ts b/spec/unit/matrixrtc/types.spec.ts index 179d16c6b1..2fb572e7a9 100644 --- a/spec/unit/matrixrtc/types.spec.ts +++ b/spec/unit/matrixrtc/types.spec.ts @@ -58,14 +58,13 @@ describe("IRTCNotificationContent", () => { expect(res.lifetime).toBe(120000); }); - it("throws on missing m.mentions", () => { + it("throws on malformed m.mentions", () => { expect(() => parseCallNotificationContent({ - notification_type: "notification", - sender_ts: 123, - lifetime: 1000, + ...validBase, + "m.mentions": "not an object", } as any), - ).toThrow("Missing m.mentions"); + ).toThrow("malformed m.mentions"); }); it("throws on missing or invalid notification_type", () => { @@ -116,15 +115,6 @@ describe("IRTCNotificationContent", () => { ).toThrow("Missing or invalid lifetime"); }); - it("throws on invalid decline_reason type", () => { - expect(() => - parseCallNotificationContent({ - ...validBase, - decline_reason: 42 as any, - } as any), - ).toThrow("Invalid decline_reason"); - }); - it("accepts valid relation (m.reference)", () => { // Note: parseCallNotificationContent currently checks `relation.rel_type` rather than `m.relates_to`. const res = parseCallNotificationContent({ diff --git a/src/matrixrtc/types.ts b/src/matrixrtc/types.ts index 4f4d444f5c..98d057b25a 100644 --- a/src/matrixrtc/types.ts +++ b/src/matrixrtc/types.ts @@ -111,8 +111,8 @@ export type RTCCallIntent = "audio" | "video" | string; * @returns a parsed IRTCNotificationContent */ export function parseCallNotificationContent(content: IContent): IRTCNotificationContent { - if (typeof content["m.mentions"] !== "object") { - throw new Error("Missing m.mentions"); + if (content["m.mentions"] && typeof content["m.mentions"] !== "object") { + throw new Error("malformed m.mentions"); } if (typeof content["notification_type"] !== "string") { throw new Error("Missing or invalid notification_type"); @@ -124,9 +124,6 @@ export function parseCallNotificationContent(content: IContent): IRTCNotificatio throw new Error("Missing or invalid lifetime"); } - if (content["decline_reason"] && typeof content["decline_reason"] !== "string") { - throw new Error("Invalid decline_reason"); - } if (content["relation"] && content["relation"]["rel_type"] !== "m.reference") { throw new Error("Invalid relation"); } @@ -143,8 +140,7 @@ export function parseCallNotificationContent(content: IContent): IRTCNotificatio * Don't cast event content to this directly. Use `parseCallNotificationContent` instead to validate the content first. */ export interface IRTCNotificationContent extends RelationEvent { - "m.mentions": IMentions; - "decline_reason"?: string; + "m.mentions"?: IMentions; "notification_type": RTCNotificationType; /** * The initial intent of the calling user.