Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions spec/unit/matrixrtc/types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
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 malformed m.mentions", () => {
expect(() =>
parseCallNotificationContent({
...validBase,
"m.mentions": "not an object",
} as any),
).toThrow("malformed 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("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");
});
});
42 changes: 39 additions & 3 deletions src/matrixrtc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -102,9 +102,45 @@ 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 (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");
}
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["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 { ...content, lifetime: cappedLifetime } 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;
"m.mentions"?: IMentions;
"notification_type": RTCNotificationType;
/**
* The initial intent of the calling user.
Expand Down