diff --git a/spec/unit/matrixrtc/CallMembership.spec.ts b/spec/unit/matrixrtc/CallMembership.spec.ts index 7b10828c1..2869b9078 100644 --- a/spec/unit/matrixrtc/CallMembership.spec.ts +++ b/spec/unit/matrixrtc/CallMembership.spec.ts @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { SessionMembershipData } from "../../../src/matrixrtc/membership/legacy"; +import { type SessionMembershipData } from "../../../src/matrixrtc/membership/legacy"; import { EventType, type MatrixEvent } from "../../../src"; import { CallMembership, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership"; import { sessionMembershipTemplate } from "./mocks"; -import { RtcMembershipData } from "../../../src/matrixrtc/membership/rtc"; +import { type RtcMembershipData } from "../../../src/matrixrtc/membership/rtc"; function makeMockEvent( eventType: EventType.RTCMembership | EventType.GroupCallMemberPrefix, diff --git a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts index afa6da7bb..65d4b67b5 100644 --- a/spec/unit/matrixrtc/MatrixRTCSession.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSession.spec.ts @@ -26,7 +26,7 @@ import { } from "../../../src"; import { KnownMembership } from "../../../src/@types/membership"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; -import { SlotDescription, Status, type EncryptionKeysEventContent } from "../../../src/matrixrtc/types"; +import { type SlotDescription, Status, type EncryptionKeysEventContent } from "../../../src/matrixrtc/types"; import { makeMockEvent, makeMockRoom, diff --git a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts index 90a93ae06..acbfbdd07 100644 --- a/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts +++ b/spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts @@ -160,7 +160,6 @@ describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])( }, ]; const room2 = makeMockRoom(room2MembershipData, eventKind === "sticky", slotDescription); - console.log({ room2: room2.roomId }); jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]); client.emit(ClientEvent.Room, room2); expect(onStarted).toHaveBeenCalled(); diff --git a/spec/unit/matrixrtc/MembershipManager.spec.ts b/spec/unit/matrixrtc/MembershipManager.spec.ts index 78fc005d0..fc565ec7d 100644 --- a/spec/unit/matrixrtc/MembershipManager.spec.ts +++ b/spec/unit/matrixrtc/MembershipManager.spec.ts @@ -28,8 +28,8 @@ import { import { MembershipManagerEvent, Status, type Transport } from "../../../src/matrixrtc"; import { makeMockClient, makeMockRoom, sessionMembershipTemplate, mockCallMembership, type MockClient } from "./mocks"; import { LegacyMembershipManager, StickyEventMembershipManager } from "../../../src/matrixrtc/MembershipManager.ts"; -import { SessionMembershipData } from "../../../src/matrixrtc/membership/legacy.ts"; -import { RtcMembershipData } from "../../../src/matrixrtc/membership/rtc.ts"; +import { type SessionMembershipData } from "../../../src/matrixrtc/membership/legacy.ts"; +import { type RtcMembershipData } from "../../../src/matrixrtc/membership/rtc.ts"; /** * Create a promise that will resolve once a mocked method is called. diff --git a/spec/unit/matrixrtc/mocks.ts b/spec/unit/matrixrtc/mocks.ts index 02518378d..6c9513e37 100644 --- a/spec/unit/matrixrtc/mocks.ts +++ b/spec/unit/matrixrtc/mocks.ts @@ -22,8 +22,8 @@ import { CallMembership } from "../../../src/matrixrtc/CallMembership"; import { secureRandomString } from "../../../src/randomstring"; import { DefaultCallApplicationDescription, - RtcSlotEventContent, - SlotDescription, + type RtcSlotEventContent, + type SlotDescription, slotDescriptionToId, } from "../../../src/matrixrtc"; import { mkMatrixEvent } from "../../../src/testing"; diff --git a/src/@types/event.ts b/src/@types/event.ts index 8a40ee5f5..7b2fc889d 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -63,8 +63,8 @@ import { type LocalNotificationSettings } from "./local_notifications.ts"; import { type IPushRules } from "./PushRules.ts"; import { type SecretInfo, type SecretStorageKeyDescription } from "../secret-storage.ts"; import { type POLICIES_ACCOUNT_EVENT_TYPE } from "../models/invites-ignorer-types.ts"; -import { RtcMembershipData } from "../matrixrtc/membership/rtc.ts"; -import { SessionMembershipData } from "../matrixrtc/membership/legacy.ts"; +import { type RtcMembershipData } from "../matrixrtc/membership/rtc.ts"; +import { type SessionMembershipData } from "../matrixrtc/membership/legacy.ts"; export enum EventType { // Room state events @@ -161,7 +161,7 @@ export enum EventType { CallNotify = "org.matrix.msc4075.call.notify", RTCNotification = "org.matrix.msc4075.rtc.notification", RTCDecline = "org.matrix.msc4310.rtc.decline", - RTCSlot = "org.matrix.msc4143.rtc.slot" + RTCSlot = "org.matrix.msc4143.rtc.slot", } export enum RelationType { diff --git a/src/matrixrtc/CallApplication.ts b/src/matrixrtc/CallApplication.ts index 901d0460d..301e2a4d0 100644 --- a/src/matrixrtc/CallApplication.ts +++ b/src/matrixrtc/CallApplication.ts @@ -1,8 +1,8 @@ -import { RtcSlotEventContent, SlotDescription } from "./types"; +import { type RtcSlotEventContent, type SlotDescription } from "./types.ts"; export const DefaultCallApplicationDescription: SlotDescription = { id: "", - application: "m.call" + application: "m.call", }; /** @@ -13,14 +13,14 @@ export interface CallSlotEventContent extends RtcSlotEventContent<"m.call"> { "type": "m.call"; "m.call.id"?: string; }; - slot_id: `${string}#${string}`, + slot_id: `${string}#${string}`; } /** * Default slot for a room using "m.call". */ export const DefaultCallApplicationSlot: CallSlotEventContent = { application: { - "type": "m.call", + type: "m.call", }, slot_id: "m.call#", }; diff --git a/src/matrixrtc/CallMembership.ts b/src/matrixrtc/CallMembership.ts index 1c9c8be39..5087c1fc3 100644 --- a/src/matrixrtc/CallMembership.ts +++ b/src/matrixrtc/CallMembership.ts @@ -19,8 +19,8 @@ import { type RTCCallIntent, type Transport, type SlotDescription } from "./type import { type IContent, type MatrixEvent } from "../models/event.ts"; import { logger } from "../logger.ts"; import { slotDescriptionToId, slotIdToDescription } from "./utils.ts"; -import { checkSessionsMembershipData, SessionMembershipData } from "./membership/legacy.ts"; -import { checkRtcMembershipData, RtcMembershipData } from "./membership/rtc.ts"; +import { checkSessionsMembershipData, type SessionMembershipData } from "./membership/legacy.ts"; +import { checkRtcMembershipData, type RtcMembershipData } from "./membership/rtc.ts"; import { EventType } from "../matrix.ts"; import { MatrixRTCMembershipParseError } from "./membership/common.ts"; @@ -45,8 +45,9 @@ enum MembershipKind { Session = "session", } - -type MembershipData = { kind: MembershipKind.RTC; data: RtcMembershipData } | { kind: MembershipKind.Session; data: SessionMembershipData }; +type MembershipData = + | { kind: MembershipKind.RTC; data: RtcMembershipData } + | { kind: MembershipKind.Session; data: SessionMembershipData }; // TODO: Rename to RtcMembership once we removed the legacy SessionMembership from this file. export class CallMembership { public static equal(a?: CallMembership, b?: CallMembership): boolean { @@ -72,6 +73,7 @@ export class CallMembership { if (sender === undefined) throw new Error("parentEvent is missing sender field"); try { + // Event types are strictly checked here. if (evType === EventType.RTCMembership && checkRtcMembershipData(data, sender)) { this.membershipData = { kind: MembershipKind.RTC, data }; } else if (evType === EventType.GroupCallMemberPrefix && checkSessionsMembershipData(data)) { @@ -171,7 +173,7 @@ export class CallMembership { return data.application; case MembershipKind.Session: default: - // XXX: This is a hack around + // XXX: This is a hack around return { "type": data.application, "m.call.intent": data["m.call.intent"] }; } } @@ -249,8 +251,9 @@ export class CallMembership { const { kind } = this.membershipData; switch (kind) { case MembershipKind.RTC: - console.log("isExpired", this.matrixEvent.unstableStickyExpiresAt, Date.now()); - return this.matrixEvent.unstableStickyExpiresAt ? Date.now() > this.matrixEvent.unstableStickyExpiresAt: false; + return this.matrixEvent.unstableStickyExpiresAt + ? Date.now() > this.matrixEvent.unstableStickyExpiresAt + : false; case MembershipKind.Session: default: return this.getMsUntilExpiry()! <= 0; diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 68bce45b6..4740dac19 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -50,7 +50,7 @@ import { type MatrixEvent } from "../models/event.ts"; import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../models/room-sticky-events.ts"; import { DefaultCallApplicationSlot } from "./CallApplication.ts"; import { slotDescriptionToId } from "./utils.ts"; -import { RtcMembershipData } from "./membership/rtc.ts"; +import { type RtcMembershipData } from "./membership/rtc.ts"; import { RoomKeyTransport } from "./RoomKeyTransport.ts"; /** @@ -300,7 +300,7 @@ export class MatrixRTCSession extends TypedEventEmitter< * @returns The contents of the slot event, or null if no matching slot found. */ public static getRtcSlot( - room: Pick, + room: Pick, slotDescription: SlotDescription, ): RtcSlotEventContent | null { const slotId = slotDescriptionToId(slotDescription); @@ -325,8 +325,8 @@ export class MatrixRTCSession extends TypedEventEmitter< } if ( - "slot_id" in slotContent === false || - typeof slotContent.slot_id !== "string" || + "slot_id" in slotContent === false || + typeof slotContent.slot_id !== "string" || !slotContent.slot_id.startsWith(slotContent.application.type + "#") ) { logger.debug(`Mismatched app for ${room.roomId}`, slotContent); @@ -361,17 +361,14 @@ export class MatrixRTCSession extends TypedEventEmitter< // Has a slot and the application parameters match, fetch sticky members. callMemberEvents = [...room._unstable_getStickyEvents()].filter((e) => { if (e.getType() !== EventType.RTCMembership) { - console.log("Invalid type"); return false; } const content = e.getContent(); // Ensure the slot ID of the membership matches the state if (content.slot_id !== slotId) { - console.log("Invalid slot ID", content.slot_id, slotId); return false; } if (content.application.type !== slotDescription.application) { - console.log("Invalid application.type", content.application.type, slotDescription.application); return false; } return true; @@ -453,7 +450,6 @@ export class MatrixRTCSession extends TypedEventEmitter< ); } - return callMemberships; } @@ -612,7 +608,13 @@ export class MatrixRTCSession extends TypedEventEmitter< this.slotDescription, this.logger, ) - : new LegacyMembershipManager(joinConfig, this.roomSubset, this.client, this.slotDescription, this.logger); + : new LegacyMembershipManager( + joinConfig, + this.roomSubset, + this.client, + this.slotDescription, + this.logger, + ); this.reEmitter.reEmit(this.membershipManager!, [ MembershipManagerEvent.ProbablyLeft, diff --git a/src/matrixrtc/MembershipManager.ts b/src/matrixrtc/MembershipManager.ts index 9112dde9a..6ad4289dc 100644 --- a/src/matrixrtc/MembershipManager.ts +++ b/src/matrixrtc/MembershipManager.ts @@ -26,17 +26,10 @@ import type { MatrixClient } from "../client.ts"; import { ConnectionError, HTTPError, MatrixError } from "../http-api/errors.ts"; import { type Logger, logger as rootLogger } from "../logger.ts"; import { type Room } from "../models/room.ts"; -import { - type CallMembership, - DEFAULT_EXPIRE_DURATION, -} from "./CallMembership.ts"; -import type { - RtcMembershipData, -} from "./membership/rtc.ts" -import type { - SessionMembershipData, -} from "./membership/legacy.ts" -import { type Transport, isMyMembership, type RTCCallIntent, Status, SlotDescription } from "./types.ts"; +import { type CallMembership, DEFAULT_EXPIRE_DURATION } from "./CallMembership.ts"; +import type { RtcMembershipData } from "./membership/rtc.ts"; +import type { SessionMembershipData } from "./membership/legacy.ts"; +import { type Transport, isMyMembership, type RTCCallIntent, Status, type SlotDescription } from "./types.ts"; import { type MembershipConfig, type SessionConfig } from "./MatrixRTCSession.ts"; import { ActionScheduler, type ActionUpdate } from "./MembershipManagerActionScheduler.ts"; import { TypedEventEmitter } from "../models/typed-event-emitter.ts"; @@ -185,7 +178,7 @@ type MembershipManagerClient = Pick< * - Stop the timer for the delay refresh * - Stop the timer for updating the state event */ -export abstract class MembershipManager +export abstract class MembershipManager extends TypedEventEmitter implements IMembershipManager { @@ -1024,25 +1017,22 @@ export class LegacyMembershipManager extends MembershipManager Promise = (myMembership) => { - return this.client.sendStateEvent( - this.room.roomId, - EventType.GroupCallMemberPrefix, - myMembership, - this.memberId, - ); - }; - + protected clientSendMembership: (myMembership: SessionMembershipData | EmptyObject) => Promise = + (myMembership) => { + return this.client.sendStateEvent( + this.room.roomId, + EventType.GroupCallMemberPrefix, + myMembership, + this.memberId, + ); + }; } /** * Implementation of the Membership manager that uses sticky events * rather than state events. - * - * This exclusively sends RTCMembershipData + * + * This exclusively sends RTCMembershipData */ export class StickyEventMembershipManager extends MembershipManager { public constructor( @@ -1068,9 +1058,9 @@ export class StickyEventMembershipManager extends MembershipManager Promise = (myMembership) => { + protected clientSendMembership: (myMembership: RtcMembershipData | EmptyObject) => Promise = ( + myMembership, + ) => { return this.clientWithSticky._unstable_sendStickyEvent( this.room.roomId, MEMBERSHIP_STICKY_DURATION_MS, diff --git a/src/matrixrtc/membership/common.ts b/src/matrixrtc/membership/common.ts index c492fd2dd..81bcd9522 100644 --- a/src/matrixrtc/membership/common.ts +++ b/src/matrixrtc/membership/common.ts @@ -1,5 +1,11 @@ +/** + * Thrown when an event does not look valid for use with MatrixRTC. + */ export class MatrixRTCMembershipParseError extends Error { - constructor(public readonly type: string, public readonly errors: string[]) { + public constructor( + public readonly type: string, + public readonly errors: string[], + ) { super(`Does not match ${type}:\n${errors.join("\n")}`); } -} \ No newline at end of file +} diff --git a/src/matrixrtc/membership/legacy.ts b/src/matrixrtc/membership/legacy.ts index 3574403ca..8aac1ba85 100644 --- a/src/matrixrtc/membership/legacy.ts +++ b/src/matrixrtc/membership/legacy.ts @@ -1,6 +1,6 @@ -import { EventType, IContent } from "../../matrix"; -import { RTCCallIntent, Transport } from "../types"; -import { MatrixRTCMembershipParseError } from "./common"; +import { EventType, type IContent } from "../../matrix.ts"; +import { type RTCCallIntent, type Transport } from "../types.ts"; +import { MatrixRTCMembershipParseError } from "./common.ts"; /** * **Legacy** (MatrixRTC) session membership data. @@ -31,8 +31,8 @@ export type SessionMembershipData = { * NOTE: This is still included for legacy reasons, but not consumed by the SDK. */ "focus_active": { - type: string, - focus_selection: "oldest_membership"|string, + type: string; + focus_selection: "oldest_membership" | string; }; /** @@ -72,6 +72,12 @@ export type SessionMembershipData = { "m.call.intent"?: RTCCallIntent; }; +/** + * Validates that `data` matches the format expected by the legacy form of MSC4143. + * @param data The event content. + * @returns true if `data` is valid SessionMembershipData + * @throws {MatrixRTCMembershipParseError} if the content is not valid + */ export const checkSessionsMembershipData = (data: IContent): data is SessionMembershipData => { const prefix = " - "; const errors: string[] = []; @@ -108,8 +114,8 @@ export const checkSessionsMembershipData = (data: IContent): data is SessionMemb } if (errors.length) { - throw new MatrixRTCMembershipParseError(EventType.GroupCallMemberPrefix, errors) + throw new MatrixRTCMembershipParseError(EventType.GroupCallMemberPrefix, errors); } return true; -}; \ No newline at end of file +}; diff --git a/src/matrixrtc/membership/rtc.ts b/src/matrixrtc/membership/rtc.ts index 3ce60bdec..e12c7ebe0 100644 --- a/src/matrixrtc/membership/rtc.ts +++ b/src/matrixrtc/membership/rtc.ts @@ -1,7 +1,6 @@ -import { EventType, IContent, MXID_PATTERN, RelationType } from "../../matrix"; -import { RtcSlotEventContent, Transport } from "../types"; -import { MatrixRTCMembershipParseError } from "./common"; - +import { EventType, type IContent, MXID_PATTERN, type RelationType } from "../../matrix.ts"; +import { type RtcSlotEventContent, type Transport } from "../types.ts"; +import { MatrixRTCMembershipParseError } from "./common.ts"; /** * Represents the current form of MSC4143. @@ -11,7 +10,7 @@ export interface RtcMembershipData { "member": { claimed_user_id: string; claimed_device_id: string; - id: string + id: string; }; "m.relates_to"?: { event_id: string; @@ -24,10 +23,14 @@ export interface RtcMembershipData { "sticky_key"?: string; } -export const checkRtcMembershipData = ( - data: IContent, - referenceUserId: string, -): data is RtcMembershipData => { +/** + * Validates that `data` matches the format expected by MSC4143. + * @param data The event content. + * @param sender The sender of the event. + * @returns true if `data` is valid RtcMembershipData + * @throws {MatrixRTCMembershipParseError} if the content is not valid + */ +export const checkRtcMembershipData = (data: IContent, sender: string): data is RtcMembershipData => { const errors: string[] = []; const prefix = " - "; @@ -40,13 +43,20 @@ export const checkRtcMembershipData = ( if (typeof data.member !== "object" || data.member === null) { errors.push(prefix + "member must be an object"); } else { - if (typeof data.member.claimed_user_id !== "string") errors.push(prefix + "member.claimed_user_id must be string"); - else if (!MXID_PATTERN.test(data.member.claimed_user_id)) errors.push(prefix + "member.claimed_user_id must be a valid mxid"); + if (typeof data.member.claimed_user_id !== "string") { + errors.push(prefix + "member.claimed_user_id must be string"); + } else if (!MXID_PATTERN.test(data.member.claimed_user_id)) { + errors.push(prefix + "member.claimed_user_id must be a valid mxid"); + } // This is not what the spec enforces but there currently are no rules what power levels are required to // send a m.rtc.member event for a other user. So we add this check for simplicity and to avoid possible attacks until there // is a proper definition when this is allowed. - else if (data.member.claimed_user_id !== referenceUserId) errors.push(prefix + "member.claimed_user_id must match the sender"); - if (typeof data.member.claimed_device_id !== "string") errors.push(prefix + "member.claimed_device_id must be string"); + else if (data.member.claimed_user_id !== sender) { + errors.push(prefix + "member.claimed_user_id must match the sender"); + } + if (typeof data.member.claimed_device_id !== "string") { + errors.push(prefix + "member.claimed_device_id must be string"); + } if (typeof data.member.id !== "string") errors.push(prefix + "member.id must be string"); } if (typeof data.application !== "object" || data.application === null) { @@ -103,8 +113,8 @@ export const checkRtcMembershipData = ( } if (errors.length) { - throw new MatrixRTCMembershipParseError(EventType.RTCMembership, errors) + throw new MatrixRTCMembershipParseError(EventType.RTCMembership, errors); } return true; -}; \ No newline at end of file +}; diff --git a/src/matrixrtc/types.ts b/src/matrixrtc/types.ts index 569927c4f..1466448b8 100644 --- a/src/matrixrtc/types.ts +++ b/src/matrixrtc/types.ts @@ -207,7 +207,7 @@ export interface RtcSlotEventContent { // other application specific keys [key: string]: unknown; }; - slot_id: string, + slot_id: string; } /**