1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-23 17:02:25 +03:00

Add the parent event to the CallMembership.

Signed-off-by: Timo K <toger5@hotmail.de>
This commit is contained in:
Timo K
2025-10-02 12:48:05 +02:00
parent 4643844597
commit 8a21ff69bb
3 changed files with 194 additions and 174 deletions

View File

@@ -23,11 +23,12 @@ import {
} from "../../../src/matrixrtc/CallMembership"; } from "../../../src/matrixrtc/CallMembership";
import { membershipTemplate } from "./mocks"; import { membershipTemplate } from "./mocks";
function makeMockEvent(originTs = 0): MatrixEvent { function makeMockEvent(originTs = 0, content = {}): MatrixEvent {
return { return {
getTs: jest.fn().mockReturnValue(originTs), getTs: jest.fn().mockReturnValue(originTs),
getSender: jest.fn().mockReturnValue("@alice:example.org"), getSender: jest.fn().mockReturnValue("@alice:example.org"),
getId: jest.fn().mockReturnValue("$eventid"), getId: jest.fn().mockReturnValue("$eventid"),
getContent: jest.fn().mockReturnValue(content),
} as unknown as MatrixEvent; } as unknown as MatrixEvent;
} }
@@ -53,63 +54,64 @@ describe("CallMembership", () => {
it("rejects membership with no device_id", () => { it("rejects membership with no device_id", () => {
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined })); new CallMembership(makeMockEvent(0, Object.assign({}, membershipTemplate, { device_id: undefined })));
}).toThrow(); }).toThrow();
}); });
it("rejects membership with no call_id", () => { it("rejects membership with no call_id", () => {
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined })); new CallMembership(makeMockEvent(0, Object.assign({}, membershipTemplate, { call_id: undefined })));
}).toThrow(); }).toThrow();
}); });
it("allow membership with no scope", () => { it("allow membership with no scope", () => {
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined })); new CallMembership(makeMockEvent(0, Object.assign({}, membershipTemplate, { scope: undefined })));
}).not.toThrow(); }).not.toThrow();
}); });
it("uses event timestamp if no created_ts", () => { it("uses event timestamp if no created_ts", () => {
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate); const membership = new CallMembership(makeMockEvent(12345, membershipTemplate));
expect(membership.createdTs()).toEqual(12345); expect(membership.createdTs()).toEqual(12345);
}); });
it("uses created_ts if present", () => { it("uses created_ts if present", () => {
const membership = new CallMembership( const membership = new CallMembership(
makeMockEvent(12345), makeMockEvent(12345, Object.assign({}, membershipTemplate, { created_ts: 67890 })),
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
); );
expect(membership.createdTs()).toEqual(67890); expect(membership.createdTs()).toEqual(67890);
}); });
it("considers memberships unexpired if local age low enough", () => { it("considers memberships unexpired if local age low enough", () => {
const fakeEvent = makeMockEvent(1000); const fakeEvent = makeMockEvent(1000, membershipTemplate);
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1)); fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false); expect(new CallMembership(fakeEvent).isExpired()).toEqual(false);
}); });
it("considers memberships expired if local age large enough", () => { it("considers memberships expired if local age large enough", () => {
const fakeEvent = makeMockEvent(1000); const fakeEvent = makeMockEvent(1000, membershipTemplate);
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1)); fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true); expect(new CallMembership(fakeEvent).isExpired()).toEqual(true);
}); });
it("returns preferred foci", () => { it("returns preferred foci", () => {
const fakeEvent = makeMockEvent();
const mockFocus = { type: "this_is_a_mock_focus" }; const mockFocus = { type: "this_is_a_mock_focus" };
const membership = new CallMembership(fakeEvent, { ...membershipTemplate, foci_preferred: [mockFocus] }); const fakeEvent = makeMockEvent(0, { ...membershipTemplate, foci_preferred: [mockFocus] });
const membership = new CallMembership(fakeEvent);
expect(membership.transports).toEqual([mockFocus]); expect(membership.transports).toEqual([mockFocus]);
}); });
describe("getTransport", () => { describe("getTransport", () => {
const mockFocus = { type: "this_is_a_mock_focus" }; const mockFocus = { type: "this_is_a_mock_focus" };
const oldestMembership = new CallMembership(makeMockEvent(), membershipTemplate); const oldestMembership = new CallMembership(makeMockEvent(0, membershipTemplate));
it("gets the correct active transport with oldest_membership", () => { it("gets the correct active transport with oldest_membership", () => {
const membership = new CallMembership(makeMockEvent(), { const membership = new CallMembership(
...membershipTemplate, makeMockEvent(0, {
foci_preferred: [mockFocus], ...membershipTemplate,
focus_active: { type: "livekit", focus_selection: "oldest_membership" }, foci_preferred: [mockFocus],
}); focus_active: { type: "livekit", focus_selection: "oldest_membership" },
}),
);
// if we are the oldest member we use our focus. // if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toStrictEqual(mockFocus); expect(membership.getTransport(membership)).toStrictEqual(mockFocus);
@@ -119,11 +121,13 @@ describe("CallMembership", () => {
}); });
it("gets the correct active transport with multi_sfu", () => { it("gets the correct active transport with multi_sfu", () => {
const membership = new CallMembership(makeMockEvent(), { const membership = new CallMembership(
...membershipTemplate, makeMockEvent(0, {
foci_preferred: [mockFocus], ...membershipTemplate,
focus_active: { type: "livekit", focus_selection: "multi_sfu" }, foci_preferred: [mockFocus],
}); focus_active: { type: "livekit", focus_selection: "multi_sfu" },
}),
);
// if we are the oldest member we use our focus. // if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toStrictEqual(mockFocus); expect(membership.getTransport(membership)).toStrictEqual(mockFocus);
@@ -132,18 +136,20 @@ describe("CallMembership", () => {
expect(membership.getTransport(oldestMembership)).toBe(mockFocus); expect(membership.getTransport(oldestMembership)).toBe(mockFocus);
}); });
it("does not provide focus if the selection method is unknown", () => { it("does not provide focus if the selection method is unknown", () => {
const membership = new CallMembership(makeMockEvent(), { const membership = new CallMembership(
...membershipTemplate, makeMockEvent(0, {
foci_preferred: [mockFocus], ...membershipTemplate,
focus_active: { type: "livekit", focus_selection: "unknown" }, foci_preferred: [mockFocus],
}); focus_active: { type: "livekit", focus_selection: "unknown" },
}),
);
// if we are the oldest member we use our focus. // if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toBeUndefined(); expect(membership.getTransport(membership)).toBeUndefined();
}); });
}); });
describe("correct values from computed fields", () => { describe("correct values from computed fields", () => {
const membership = new CallMembership(makeMockEvent(), membershipTemplate); const membership = new CallMembership(makeMockEvent(0, membershipTemplate));
it("returns correct sender", () => { it("returns correct sender", () => {
expect(membership.sender).toBe("@alice:example.org"); expect(membership.sender).toBe("@alice:example.org");
}); });
@@ -192,58 +198,68 @@ describe("CallMembership", () => {
it("rejects membership with no slot_id", () => { it("rejects membership with no slot_id", () => {
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: undefined }); new CallMembership(makeMockEvent(0, { ...membershipTemplate, slot_id: undefined }));
}).toThrow(); }).toThrow();
}); });
it("rejects membership with no application", () => { it("rejects membership with no application", () => {
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, application: undefined }); new CallMembership(makeMockEvent(0, { ...membershipTemplate, application: undefined }));
}).toThrow(); }).toThrow();
}); });
it("rejects membership with incorrect application", () => { it("rejects membership with incorrect application", () => {
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), { new CallMembership(
...membershipTemplate, makeMockEvent(0, {
application: { wrong_type_key: "unknown" }, ...membershipTemplate,
}); application: { wrong_type_key: "unknown" },
}),
);
}).toThrow(); }).toThrow();
}); });
it("rejects membership with no member", () => { it("rejects membership with no member", () => {
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, member: undefined }); new CallMembership(makeMockEvent(0, { ...membershipTemplate, member: undefined }));
}).toThrow(); }).toThrow();
}); });
it("rejects membership with incorrect member", () => { it("rejects membership with incorrect member", () => {
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, member: { i: "test" } }); new CallMembership(makeMockEvent(0, { ...membershipTemplate, member: { i: "test" } }));
}).toThrow(); }).toThrow();
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), { new CallMembership(
...membershipTemplate, makeMockEvent(0, {
member: { id: "test", device_id: "test", user_id_wrong: "test" }, ...membershipTemplate,
}); member: { id: "test", device_id: "test", user_id_wrong: "test" },
}),
);
}).toThrow(); }).toThrow();
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), { new CallMembership(
...membershipTemplate, makeMockEvent(0, {
member: { id: "test", device_id_wrong: "test", user_id_wrong: "test" }, ...membershipTemplate,
}); member: { id: "test", device_id_wrong: "test", user_id_wrong: "test" },
}),
);
}).toThrow(); }).toThrow();
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), { new CallMembership(
...membershipTemplate, makeMockEvent(0, {
member: { id: "test", device_id: "test", user_id: "@@test" }, ...membershipTemplate,
}); member: { id: "test", device_id: "test", user_id: "@@test" },
}),
);
}).toThrow(); }).toThrow();
expect(() => { expect(() => {
new CallMembership(makeMockEvent(), { new CallMembership(
...membershipTemplate, makeMockEvent(0, {
member: { id: "test", device_id: "test", user_id: "@test:user.id" }, ...membershipTemplate,
}); member: { id: "test", device_id: "test", user_id: "@test:user.id" },
}),
);
}).not.toThrow(); }).not.toThrow();
}); });
@@ -257,11 +273,13 @@ describe("CallMembership", () => {
describe("getTransport", () => { describe("getTransport", () => {
it("gets the correct active transport with oldest_membership", () => { it("gets the correct active transport with oldest_membership", () => {
const oldestMembership = new CallMembership(makeMockEvent(), { const oldestMembership = new CallMembership(
...membershipTemplate, makeMockEvent(0, {
rtc_transports: [{ type: "oldest_transport" }], ...membershipTemplate,
}); rtc_transports: [{ type: "oldest_transport" }],
const membership = new CallMembership(makeMockEvent(), membershipTemplate); }),
);
const membership = new CallMembership(makeMockEvent(0, membershipTemplate));
// if we are the oldest member we use our focus. // if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toStrictEqual({ type: "livekit" }); expect(membership.getTransport(membership)).toStrictEqual({ type: "livekit" });
@@ -271,7 +289,7 @@ describe("CallMembership", () => {
}); });
}); });
describe("correct values from computed fields", () => { describe("correct values from computed fields", () => {
const membership = new CallMembership(makeMockEvent(), membershipTemplate); const membership = new CallMembership(makeMockEvent(0, membershipTemplate));
it("returns correct sender", () => { it("returns correct sender", () => {
expect(membership.sender).toBe("@alice:example.org"); expect(membership.sender).toBe("@alice:example.org");
}); });
@@ -304,9 +322,9 @@ describe("CallMembership", () => {
it("returns correct membershipID", () => { it("returns correct membershipID", () => {
expect(membership.membershipID).toBe("xyzHASHxyz"); expect(membership.membershipID).toBe("xyzHASHxyz");
}); });
it("returns correct unused fields", () => { it("returns correct expiration fields", () => {
expect(membership.getAbsoluteExpiry()).toBe(undefined); expect(membership.getAbsoluteExpiry()).toBe(DEFAULT_EXPIRE_DURATION);
expect(membership.getMsUntilExpiry()).toBe(undefined); expect(membership.getMsUntilExpiry()).toBe(DEFAULT_EXPIRE_DURATION - Date.now());
expect(membership.isExpired()).toBe(false); expect(membership.isExpired()).toBe(false);
}); });
}); });
@@ -318,8 +336,8 @@ describe("CallMembership", () => {
beforeEach(() => { beforeEach(() => {
// server origin timestamp for this event is 1000 // server origin timestamp for this event is 1000
fakeEvent = makeMockEvent(1000); fakeEvent = makeMockEvent(1000, membershipTemplate);
membership = new CallMembership(fakeEvent!, membershipTemplate); membership = new CallMembership(fakeEvent!);
jest.useFakeTimers(); jest.useFakeTimers();
}); });

View File

@@ -223,11 +223,17 @@ export class CallMembership {
* To access checked eventId and sender from the matrixEvent. * To access checked eventId and sender from the matrixEvent.
* Class construction will fail if these values cannot get obtained. */ * Class construction will fail if these values cannot get obtained. */
private matrixEventData: { eventId: string; sender: string }; private matrixEventData: { eventId: string; sender: string };
/**
* Constructs a CallMembership from a Matrix event.
* @param matrixEvent The Matrix event that this membership is based on
* @param relatedEvent The fetched event linked via the `event_id` from the `m.relates_to` field if present.
* @throws if the data does not match any known membership format.
*/
public constructor( public constructor(
/** The Matrix event that this membership is based on */
private matrixEvent: MatrixEvent, private matrixEvent: MatrixEvent,
data: any, private relatedEvent?: MatrixEvent,
) { ) {
const data = matrixEvent.getContent() as any;
const sessionErrors: string[] = []; const sessionErrors: string[] = [];
const rtcErrors: string[] = []; const rtcErrors: string[] = [];
if (checkSessionsMembershipData(data, sessionErrors)) { if (checkSessionsMembershipData(data, sessionErrors)) {
@@ -354,8 +360,7 @@ export class CallMembership {
const { kind, data } = this.membershipData; const { kind, data } = this.membershipData;
switch (kind) { switch (kind) {
case "rtc": case "rtc":
// TODO we need to read the referenced (relation) event if available to get the real created_ts return this.relatedEvent?.getTs() ?? this.matrixEvent.getTs();
return this.matrixEvent.getTs();
case "session": case "session":
default: default:
return data.created_ts ?? this.matrixEvent.getTs(); return data.created_ts ?? this.matrixEvent.getTs();
@@ -370,7 +375,7 @@ export class CallMembership {
const { kind, data } = this.membershipData; const { kind, data } = this.membershipData;
switch (kind) { switch (kind) {
case "rtc": case "rtc":
return undefined; return this.createdTs() + DEFAULT_EXPIRE_DURATION;
case "session": case "session":
default: default:
// TODO: calculate this from the MatrixRTCSession join configuration directly // TODO: calculate this from the MatrixRTCSession join configuration directly
@@ -382,17 +387,10 @@ export class CallMembership {
* @returns The number of milliseconds until the membership expires or undefined if applicable * @returns The number of milliseconds until the membership expires or undefined if applicable
*/ */
public getMsUntilExpiry(): number | undefined { public getMsUntilExpiry(): number | undefined {
const { kind } = this.membershipData; // Assume that local clock is sufficiently in sync with other clocks in the distributed system.
switch (kind) { // We used to try and adjust for the local clock being skewed, but there are cases where this is not accurate.
case "rtc": // The current implementation allows for the local clock to be -infinity to +MatrixRTCSession.MEMBERSHIP_EXPIRY_TIME/2
return undefined; return this.getAbsoluteExpiry()! - Date.now();
case "session":
default:
// Assume that local clock is sufficiently in sync with other clocks in the distributed system.
// We used to try and adjust for the local clock being skewed, but there are cases where this is not accurate.
// The current implementation allows for the local clock to be -infinity to +MatrixRTCSession.MEMBERSHIP_EXPIRY_TIME/2
return this.getAbsoluteExpiry()! - Date.now();
}
} }
/** /**

View File

@@ -50,6 +50,7 @@ import {
} from "./RoomAndToDeviceKeyTransport.ts"; } from "./RoomAndToDeviceKeyTransport.ts";
import { TypedReEmitter } from "../ReEmitter.ts"; import { TypedReEmitter } from "../ReEmitter.ts";
import { ToDeviceKeyTransport } from "./ToDeviceKeyTransport.ts"; import { ToDeviceKeyTransport } from "./ToDeviceKeyTransport.ts";
import { MatrixEvent } from "../models/event.ts";
/** /**
* Events emitted by MatrixRTCSession * Events emitted by MatrixRTCSession
@@ -308,10 +309,10 @@ export class MatrixRTCSession extends TypedEventEmitter<
* *
* @deprecated Use `MatrixRTCSession.sessionMembershipsForSlot` instead. * @deprecated Use `MatrixRTCSession.sessionMembershipsForSlot` instead.
*/ */
public static callMembershipsForRoom( public static async callMembershipsForRoom(
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState">, room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState" | "findEventById" | "client">,
): CallMembership[] { ): Promise<CallMembership[]> {
return MatrixRTCSession.sessionMembershipsForSlot(room, { return await MatrixRTCSession.sessionMembershipsForSlot(room, {
id: "", id: "",
application: "m.call", application: "m.call",
}); });
@@ -320,21 +321,22 @@ export class MatrixRTCSession extends TypedEventEmitter<
/** /**
* @deprecated use `MatrixRTCSession.slotMembershipsForRoom` instead. * @deprecated use `MatrixRTCSession.slotMembershipsForRoom` instead.
*/ */
public static sessionMembershipsForRoom( public static async sessionMembershipsForRoom(
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState">, room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState" | "findEventById" | "client">,
sessionDescription: SlotDescription, sessionDescription: SlotDescription,
): CallMembership[] { ): Promise<CallMembership[]> {
return this.sessionMembershipsForSlot(room, sessionDescription); return await this.sessionMembershipsForSlot(room, sessionDescription);
} }
/** /**
* Returns all the call memberships for a room that match the provided `sessionDescription`, * Returns all the call memberships for a room that match the provided `sessionDescription`,
* oldest first. * oldest first.
*/ */
public static sessionMembershipsForSlot( public static async sessionMembershipsForSlot(
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState">, room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState" | "findEventById" | "client">,
slotDescription: SlotDescription, slotDescription: SlotDescription,
): CallMembership[] { existingMemberships?: CallMembership[],
): Promise<CallMembership[]> {
const logger = rootLogger.getChild(`[MatrixRTCSession ${room.roomId}]`); const logger = rootLogger.getChild(`[MatrixRTCSession ${room.roomId}]`);
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS); const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
if (!roomState) { if (!roomState) {
@@ -342,54 +344,41 @@ export class MatrixRTCSession extends TypedEventEmitter<
throw new Error("Could't get state for room " + room.roomId); throw new Error("Could't get state for room " + room.roomId);
} }
const callMemberEvents = roomState.getStateEvents(EventType.GroupCallMemberPrefix); const callMemberEvents = roomState.getStateEvents(EventType.GroupCallMemberPrefix);
const callMemberships: CallMembership[] = []; const callMemberships: CallMembership[] = [];
for (const memberEvent of callMemberEvents) { for (const memberEvent of callMemberEvents) {
const content = memberEvent.getContent(); let membership = existingMemberships?.find((m) => m.eventId === memberEvent.getId());
const eventKeysCount = Object.keys(content).length; if (!membership) {
// Dont even bother about empty events (saves us from costly type/"key in" checks in bigger rooms) const relatedEventId = memberEvent.relationEventId;
if (eventKeysCount === 0) continue; const relatedEvent = relatedEventId
? room.findEventById(relatedEventId)
: new MatrixEvent(await room.client.fetchRoomEvent(room.roomId, relatedEventId!));
const membershipContents: any[] = [];
// We first decide if its a MSC4143 event (per device state key)
if (eventKeysCount > 1 && "focus_active" in content) {
// We have a MSC4143 event membership event
membershipContents.push(content);
} else if (eventKeysCount === 1 && "memberships" in content) {
logger.warn(`Legacy event found. Those are ignored, they do not contribute to the MatrixRTC session`);
}
if (membershipContents.length === 0) continue;
for (const membershipData of membershipContents) {
if (!("application" in membershipData)) {
// This is a left membership event, ignore it here to not log warnings.
continue;
}
try { try {
const membership = new CallMembership(memberEvent, membershipData); membership = new CallMembership(memberEvent, relatedEvent);
if (!deepCompare(membership.slotDescription, slotDescription)) {
logger.info(
`Ignoring membership of user ${membership.sender} for a different session: ${JSON.stringify(membership.slotDescription)}`,
);
continue;
}
if (membership.isExpired()) {
logger.info(`Ignoring expired device membership ${membership.sender}/${membership.deviceId}`);
continue;
}
if (!room.hasMembershipState(membership.sender ?? "", KnownMembership.Join)) {
logger.info(`Ignoring membership of user ${membership.sender} who is not in the room.`);
continue;
}
callMemberships.push(membership);
} catch (e) { } catch (e) {
logger.warn("Couldn't construct call membership: ", e); logger.warn("Couldn't construct call membership: ", e);
continue;
}
// static check for newly created memberships
if (!deepCompare(membership.slotDescription, slotDescription)) {
logger.info(
`Ignoring membership of user ${membership.sender} for a different session: ${JSON.stringify(membership.slotDescription)}`,
);
continue;
} }
} }
// Dynamic checks for all (including existing) memberships
if (membership.isExpired()) {
logger.info(`Ignoring expired device membership ${membership.sender}/${membership.deviceId}`);
continue;
}
if (!room.hasMembershipState(membership.sender ?? "", KnownMembership.Join)) {
logger.info(`Ignoring membership of user ${membership.sender} who is not in the room.`);
continue;
}
callMemberships.push(membership);
} }
callMemberships.sort((a, b) => a.createdTs() - b.createdTs()); callMemberships.sort((a, b) => a.createdTs() - b.createdTs());
@@ -413,15 +402,22 @@ export class MatrixRTCSession extends TypedEventEmitter<
* *
* @deprecated Use `MatrixRTCSession.sessionForSlot` with sessionDescription `{ id: "", application: "m.call" }` instead. * @deprecated Use `MatrixRTCSession.sessionForSlot` with sessionDescription `{ id: "", application: "m.call" }` instead.
*/ */
public static roomSessionForRoom(client: MatrixClient, room: Room): MatrixRTCSession { public static async roomSessionForRoom(client: MatrixClient, room: Room): Promise<MatrixRTCSession> {
const callMemberships = MatrixRTCSession.sessionMembershipsForSlot(room, { id: "", application: "m.call" }); const callMemberships = await MatrixRTCSession.sessionMembershipsForSlot(room, {
id: "",
application: "m.call",
});
return new MatrixRTCSession(client, room, callMemberships, { id: "", application: "m.call" }); return new MatrixRTCSession(client, room, callMemberships, { id: "", application: "m.call" });
} }
/** /**
* @deprecated Use `MatrixRTCSession.sessionForSlot` instead. * @deprecated Use `MatrixRTCSession.sessionForSlot` instead.
*/ */
public static sessionForRoom(client: MatrixClient, room: Room, slotDescription: SlotDescription): MatrixRTCSession { public static async sessionForRoom(
client: MatrixClient,
room: Room,
slotDescription: SlotDescription,
): Promise<MatrixRTCSession> {
return this.sessionForSlot(client, room, slotDescription); return this.sessionForSlot(client, room, slotDescription);
} }
@@ -430,8 +426,12 @@ export class MatrixRTCSession extends TypedEventEmitter<
* This returned session can be used to find out if there are active sessions * This returned session can be used to find out if there are active sessions
* for the requested room and `slotDescription`. * for the requested room and `slotDescription`.
*/ */
public static sessionForSlot(client: MatrixClient, room: Room, slotDescription: SlotDescription): MatrixRTCSession { public static async sessionForSlot(
const callMemberships = MatrixRTCSession.sessionMembershipsForSlot(room, slotDescription); client: MatrixClient,
room: Room,
slotDescription: SlotDescription,
): Promise<MatrixRTCSession> {
const callMemberships = await MatrixRTCSession.sessionMembershipsForSlot(room, slotDescription);
return new MatrixRTCSession(client, room, callMemberships, slotDescription); return new MatrixRTCSession(client, room, callMemberships, slotDescription);
} }
@@ -803,46 +803,50 @@ export class MatrixRTCSession extends TypedEventEmitter<
*/ */
private recalculateSessionMembers = (): void => { private recalculateSessionMembers = (): void => {
const oldMemberships = this.memberships; const oldMemberships = this.memberships;
this.memberships = MatrixRTCSession.sessionMembershipsForSlot(this.room, this.slotDescription); void MatrixRTCSession.sessionMembershipsForSlot(this.room, this.slotDescription, oldMemberships).then(
(newMemberships) => {
this.memberships = newMemberships;
this._slotId = this._slotId ?? this.memberships[0]?.slotId;
this._slotId = this._slotId ?? this.memberships[0]?.slotId; const changed =
oldMemberships.length != this.memberships.length ||
// If they have the same length, this is enough to check "changed"
oldMemberships.some((m, i) => !CallMembership.equal(m, this.memberships[i]));
const changed = if (changed) {
oldMemberships.length != this.memberships.length || this.logger.info(
oldMemberships.some((m, i) => !CallMembership.equal(m, this.memberships[i])); `Memberships for call in room ${this.roomSubset.roomId} have changed: emitting (${this.memberships.length} members)`,
if (changed) {
this.logger.info(
`Memberships for call in room ${this.roomSubset.roomId} have changed: emitting (${this.memberships.length} members)`,
);
logDurationSync(this.logger, "emit MatrixRTCSessionEvent.MembershipsChanged", () => {
this.emit(MatrixRTCSessionEvent.MembershipsChanged, oldMemberships, this.memberships);
});
void this.membershipManager?.onRTCSessionMemberUpdate(this.memberships);
// The `ownMembership` will be set when calling `onRTCSessionMemberUpdate`.
const ownMembership = this.membershipManager?.ownMembership;
if (this.pendingNotificationToSend && ownMembership && oldMemberships.length === 0) {
// If we're the first member in the call, we're responsible for
// sending the notification event
if (ownMembership.eventId && this.joinConfig?.notificationType) {
this.sendCallNotify(
ownMembership.eventId,
this.joinConfig.notificationType,
ownMembership.callIntent,
); );
} else { logDurationSync(this.logger, "emit MatrixRTCSessionEvent.MembershipsChanged", () => {
this.logger.warn("Own membership eventId is undefined, cannot send call notification"); this.emit(MatrixRTCSessionEvent.MembershipsChanged, oldMemberships, this.memberships);
} });
}
// If anyone else joins the session it is no longer our responsibility to send the notification.
// (If we were the joiner we already did sent the notification in the block above.)
if (this.memberships.length > 0) this.pendingNotificationToSend = undefined;
}
// This also needs to be done if `changed` = false
// A member might have updated their fingerprint (created_ts)
void this.encryptionManager?.onMembershipsUpdate(oldMemberships);
this.setExpiryTimer(); void this.membershipManager?.onRTCSessionMemberUpdate(this.memberships);
// The `ownMembership` will be set when calling `onRTCSessionMemberUpdate`.
const ownMembership = this.membershipManager?.ownMembership;
if (this.pendingNotificationToSend && ownMembership && oldMemberships.length === 0) {
// If we're the first member in the call, we're responsible for
// sending the notification event
if (ownMembership.eventId && this.joinConfig?.notificationType) {
this.sendCallNotify(
ownMembership.eventId,
this.joinConfig.notificationType,
ownMembership.callIntent,
);
} else {
this.logger.warn("Own membership eventId is undefined, cannot send call notification");
}
}
// If anyone else joins the session it is no longer our responsibility to send the notification.
// (If we were the joiner we already did sent the notification in the block above.)
if (this.memberships.length > 0) this.pendingNotificationToSend = undefined;
}
// This also needs to be done if `changed` = false
// A member might have updated their fingerprint (created_ts)
void this.encryptionManager?.onMembershipsUpdate(oldMemberships);
this.setExpiryTimer();
},
);
}; };
} }