Skip to content

Commit 6a15e8f

Browse files
authored
Use legacy call membership if anyone else is (#4260)
* Use legacy call membership if anyone else is * Convert nullish to boolean * Update tests * Lint * Use computed decision to use legacy events or not * Check if discovered legacy sessions are ongoing * Lint * Lint again * Increase test coverage
1 parent 238eea0 commit 6a15e8f

File tree

3 files changed

+169
-37
lines changed

3 files changed

+169
-37
lines changed

spec/unit/matrixrtc/MatrixRTCSession.spec.ts

Lines changed: 122 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ limitations under the License.
1616

1717
import { EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
1818
import { KnownMembership } from "../../../src/@types/membership";
19-
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
19+
import {
20+
CallMembershipData,
21+
CallMembershipDataLegacy,
22+
SessionMembershipData,
23+
} from "../../../src/matrixrtc/CallMembership";
2024
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
2125
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
2226
import { randomString } from "../../../src/randomstring";
@@ -99,22 +103,33 @@ describe("MatrixRTCSession", () => {
99103
});
100104

101105
it("safely ignores events with no memberships section", () => {
106+
const roomId = randomString(8);
107+
const event = {
108+
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
109+
getContent: jest.fn().mockReturnValue({}),
110+
getSender: jest.fn().mockReturnValue("@mock:user.example"),
111+
getTs: jest.fn().mockReturnValue(1000),
112+
getLocalAge: jest.fn().mockReturnValue(0),
113+
};
102114
const mockRoom = {
103115
...makeMockRoom([]),
104-
roomId: randomString(8),
116+
roomId,
105117
getLiveTimeline: jest.fn().mockReturnValue({
106118
getState: jest.fn().mockReturnValue({
107119
on: jest.fn(),
108120
off: jest.fn(),
109-
getStateEvents: (_type: string, _stateKey: string) => [
110-
{
111-
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
112-
getContent: jest.fn().mockReturnValue({}),
113-
getSender: jest.fn().mockReturnValue("@mock:user.example"),
114-
getTs: jest.fn().mockReturnValue(1000),
115-
getLocalAge: jest.fn().mockReturnValue(0),
116-
},
117-
],
121+
getStateEvents: (_type: string, _stateKey: string) => [event],
122+
events: new Map([
123+
[
124+
EventType.GroupCallMemberPrefix,
125+
{
126+
size: () => true,
127+
has: (_stateKey: string) => true,
128+
get: (_stateKey: string) => event,
129+
values: () => [event],
130+
},
131+
],
132+
]),
118133
}),
119134
}),
120135
};
@@ -123,22 +138,33 @@ describe("MatrixRTCSession", () => {
123138
});
124139

125140
it("safely ignores events with junk memberships section", () => {
141+
const roomId = randomString(8);
142+
const event = {
143+
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
144+
getContent: jest.fn().mockReturnValue({ memberships: ["i am a fish"] }),
145+
getSender: jest.fn().mockReturnValue("@mock:user.example"),
146+
getTs: jest.fn().mockReturnValue(1000),
147+
getLocalAge: jest.fn().mockReturnValue(0),
148+
};
126149
const mockRoom = {
127150
...makeMockRoom([]),
128-
roomId: randomString(8),
151+
roomId,
129152
getLiveTimeline: jest.fn().mockReturnValue({
130153
getState: jest.fn().mockReturnValue({
131154
on: jest.fn(),
132155
off: jest.fn(),
133-
getStateEvents: (_type: string, _stateKey: string) => [
134-
{
135-
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
136-
getContent: jest.fn().mockReturnValue({ memberships: "i am a fish" }),
137-
getSender: jest.fn().mockReturnValue("@mock:user.example"),
138-
getTs: jest.fn().mockReturnValue(1000),
139-
getLocalAge: jest.fn().mockReturnValue(0),
140-
},
141-
],
156+
getStateEvents: (_type: string, _stateKey: string) => [event],
157+
events: new Map([
158+
[
159+
EventType.GroupCallMemberPrefix,
160+
{
161+
size: () => true,
162+
has: (_stateKey: string) => true,
163+
get: (_stateKey: string) => event,
164+
values: () => [event],
165+
},
166+
],
167+
]),
142168
}),
143169
}),
144170
};
@@ -186,6 +212,67 @@ describe("MatrixRTCSession", () => {
186212
expect(sess.memberships).toHaveLength(0);
187213
});
188214

215+
describe("updateCallMembershipEvent", () => {
216+
const mockFocus = { type: "livekit", livekit_service_url: "https://test.org" };
217+
const joinSessionConfig = { useLegacyMemberEvents: false };
218+
219+
const legacyMembershipData: CallMembershipDataLegacy = {
220+
call_id: "",
221+
scope: "m.room",
222+
application: "m.call",
223+
device_id: "AAAAAAA_legacy",
224+
expires: 60 * 60 * 1000,
225+
membershipID: "bloop",
226+
foci_active: [mockFocus],
227+
};
228+
229+
const expiredLegacyMembershipData: CallMembershipDataLegacy = {
230+
...legacyMembershipData,
231+
device_id: "AAAAAAA_legacy_expired",
232+
expires: 0,
233+
};
234+
235+
const sessionMembershipData: SessionMembershipData = {
236+
call_id: "",
237+
scope: "m.room",
238+
application: "m.call",
239+
device_id: "AAAAAAA_session",
240+
focus_active: mockFocus,
241+
foci_preferred: [mockFocus],
242+
};
243+
244+
function testSession(
245+
membershipData: CallMembershipData[] | SessionMembershipData,
246+
shouldUseLegacy: boolean,
247+
): void {
248+
sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData));
249+
250+
const makeNewLegacyMembershipsMock = jest.spyOn(sess as any, "makeNewLegacyMemberships");
251+
const makeNewMembershipMock = jest.spyOn(sess as any, "makeNewMembership");
252+
253+
sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig);
254+
255+
expect(makeNewLegacyMembershipsMock).toHaveBeenCalledTimes(shouldUseLegacy ? 1 : 0);
256+
expect(makeNewMembershipMock).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1);
257+
}
258+
259+
it("uses legacy events if there are any active legacy calls", () => {
260+
testSession([expiredLegacyMembershipData, legacyMembershipData, sessionMembershipData], true);
261+
});
262+
263+
it('uses legacy events if a non-legacy call is in a "memberships" array', () => {
264+
testSession([sessionMembershipData], true);
265+
});
266+
267+
it("uses non-legacy events if all legacy calls are expired", () => {
268+
testSession([expiredLegacyMembershipData], false);
269+
});
270+
271+
it("uses non-legacy events if there are only non-legacy calls", () => {
272+
testSession(sessionMembershipData, false);
273+
});
274+
});
275+
189276
describe("getOldestMembership", () => {
190277
it("returns the oldest membership event", () => {
191278
const mockRoom = makeMockRoom([
@@ -340,9 +427,20 @@ describe("MatrixRTCSession", () => {
340427

341428
// definitely should have renewed by 1 second before the expiry!
342429
const timeElapsed = 60 * 60 * 1000 - 1000;
343-
mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!.getStateEvents = jest
344-
.fn()
345-
.mockReturnValue(mockRTCEvent(eventContent.memberships, mockRoom.roomId, timeElapsed));
430+
const event = mockRTCEvent(eventContent.memberships, mockRoom.roomId, timeElapsed);
431+
const getState = mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
432+
getState.getStateEvents = jest.fn().mockReturnValue(event);
433+
getState.events = new Map([
434+
[
435+
event.getType(),
436+
{
437+
size: () => true,
438+
has: (_stateKey: string) => true,
439+
get: (_stateKey: string) => event,
440+
values: () => [event],
441+
} as unknown as Map<string, MatrixEvent>,
442+
],
443+
]);
346444

347445
const eventReSentPromise = new Promise<Record<string, any>>((r) => {
348446
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {

spec/unit/matrixrtc/mocks.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ limitations under the License.
1515
*/
1616

1717
import { EventType, MatrixEvent, Room } from "../../../src";
18-
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
18+
import { CallMembershipData, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
1919
import { randomString } from "../../../src/randomstring";
2020

21-
export function makeMockRoom(memberships: CallMembershipData[], localAge: number | null = null): Room {
21+
type MembershipData = CallMembershipData[] | SessionMembershipData;
22+
23+
export function makeMockRoom(membershipData: MembershipData, localAge: number | null = null): Room {
2224
const roomId = randomString(8);
2325
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
24-
const roomState = makeMockRoomState(memberships, roomId, localAge);
26+
const roomState = makeMockRoomState(membershipData, roomId, localAge);
2527
return {
2628
roomId: roomId,
2729
hasMembershipState: jest.fn().mockReturnValue(true),
@@ -31,24 +33,39 @@ export function makeMockRoom(memberships: CallMembershipData[], localAge: number
3133
} as unknown as Room;
3234
}
3335

34-
export function makeMockRoomState(memberships: CallMembershipData[], roomId: string, localAge: number | null = null) {
35-
const event = mockRTCEvent(memberships, roomId, localAge);
36+
export function makeMockRoomState(membershipData: MembershipData, roomId: string, localAge: number | null = null) {
37+
const event = mockRTCEvent(membershipData, roomId, localAge);
3638
return {
3739
on: jest.fn(),
3840
off: jest.fn(),
3941
getStateEvents: (_: string, stateKey: string) => {
4042
if (stateKey !== undefined) return event;
4143
return [event];
4244
},
45+
events: new Map([
46+
[
47+
event.getType(),
48+
{
49+
size: () => true,
50+
has: (_stateKey: string) => true,
51+
get: (_stateKey: string) => event,
52+
values: () => [event],
53+
},
54+
],
55+
]),
4356
};
4457
}
4558

46-
export function mockRTCEvent(memberships: CallMembershipData[], roomId: string, localAge: number | null): MatrixEvent {
59+
export function mockRTCEvent(membershipData: MembershipData, roomId: string, localAge: number | null): MatrixEvent {
4760
return {
4861
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
49-
getContent: jest.fn().mockReturnValue({
50-
memberships: memberships,
51-
}),
62+
getContent: jest.fn().mockReturnValue(
63+
!Array.isArray(membershipData)
64+
? membershipData
65+
: {
66+
memberships: membershipData,
67+
},
68+
),
5269
getSender: jest.fn().mockReturnValue("@mock:user.example"),
5370
getTs: jest.fn().mockReturnValue(1000),
5471
localTimestamp: Date.now() - (localAge ?? 10),

src/matrixrtc/MatrixRTCSession.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -823,11 +823,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
823823
const localDeviceId = this.client.getDeviceId();
824824
if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!");
825825

826-
const myCallMemberEvent = roomState.getStateEvents(EventType.GroupCallMemberPrefix, localUserId) ?? undefined;
827-
const content = myCallMemberEvent?.getContent() ?? {};
828-
const legacy = "memberships" in content || this.useLegacyMemberEvents;
826+
const callMemberEvents = roomState.events.get(EventType.GroupCallMemberPrefix);
827+
const legacy =
828+
!!this.useLegacyMemberEvents ||
829+
(callMemberEvents?.size && this.stateEventsContainOngoingLegacySession(callMemberEvents));
829830
let newContent: {} | ExperimentalGroupCallRoomMemberState | SessionMembershipData = {};
830831
if (legacy) {
832+
const myCallMemberEvent = callMemberEvents?.get(localUserId);
833+
const content = myCallMemberEvent?.getContent() ?? {};
831834
let myPrevMembership: CallMembership | undefined;
832835
// We know its CallMembershipDataLegacy
833836
const memberships: CallMembershipDataLegacy[] = Array.isArray(content["memberships"])
@@ -866,7 +869,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
866869
this.room.roomId,
867870
EventType.GroupCallMemberPrefix,
868871
newContent,
869-
this.useLegacyMemberEvents ? localUserId : `${localUserId}_${localDeviceId}`,
872+
legacy ? localUserId : `${localUserId}_${localDeviceId}`,
870873
);
871874
logger.info(`Sent updated call member event.`);
872875

@@ -882,6 +885,20 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
882885
}
883886
}
884887

888+
private stateEventsContainOngoingLegacySession(callMemberEvents: Map<string, MatrixEvent>): boolean {
889+
for (const callMemberEvent of callMemberEvents.values()) {
890+
const content = callMemberEvent.getContent();
891+
if (Array.isArray(content["memberships"])) {
892+
for (const membership of content.memberships) {
893+
if (!new CallMembership(callMemberEvent, membership).isExpired()) {
894+
return true;
895+
}
896+
}
897+
}
898+
}
899+
return false;
900+
}
901+
885902
private onRotateKeyTimeout = (): void => {
886903
if (!this.manageMediaKeys) return;
887904

0 commit comments

Comments
 (0)