1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

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
This commit is contained in:
Andrew Ferrazzutti
2024-06-21 20:40:27 +09:00
committed by GitHub
parent 238eea0ef5
commit 6a15e8f1a0
3 changed files with 169 additions and 37 deletions

View File

@@ -16,7 +16,11 @@ limitations under the License.
import { EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
import { KnownMembership } from "../../../src/@types/membership";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import {
CallMembershipData,
CallMembershipDataLegacy,
SessionMembershipData,
} from "../../../src/matrixrtc/CallMembership";
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
import { randomString } from "../../../src/randomstring";
@@ -99,22 +103,33 @@ describe("MatrixRTCSession", () => {
});
it("safely ignores events with no memberships section", () => {
const mockRoom = {
...makeMockRoom([]),
roomId: randomString(8),
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue({
on: jest.fn(),
off: jest.fn(),
getStateEvents: (_type: string, _stateKey: string) => [
{
const roomId = randomString(8);
const event = {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
getLocalAge: jest.fn().mockReturnValue(0),
};
const mockRoom = {
...makeMockRoom([]),
roomId,
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue({
on: jest.fn(),
off: jest.fn(),
getStateEvents: (_type: string, _stateKey: string) => [event],
events: new Map([
[
EventType.GroupCallMemberPrefix,
{
size: () => true,
has: (_stateKey: string) => true,
get: (_stateKey: string) => event,
values: () => [event],
},
],
]),
}),
}),
};
@@ -123,22 +138,33 @@ describe("MatrixRTCSession", () => {
});
it("safely ignores events with junk memberships section", () => {
const roomId = randomString(8);
const event = {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({ memberships: ["i am a fish"] }),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
getLocalAge: jest.fn().mockReturnValue(0),
};
const mockRoom = {
...makeMockRoom([]),
roomId: randomString(8),
roomId,
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue({
on: jest.fn(),
off: jest.fn(),
getStateEvents: (_type: string, _stateKey: string) => [
getStateEvents: (_type: string, _stateKey: string) => [event],
events: new Map([
[
EventType.GroupCallMemberPrefix,
{
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({ memberships: "i am a fish" }),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
getLocalAge: jest.fn().mockReturnValue(0),
size: () => true,
has: (_stateKey: string) => true,
get: (_stateKey: string) => event,
values: () => [event],
},
],
]),
}),
}),
};
@@ -186,6 +212,67 @@ describe("MatrixRTCSession", () => {
expect(sess.memberships).toHaveLength(0);
});
describe("updateCallMembershipEvent", () => {
const mockFocus = { type: "livekit", livekit_service_url: "https://test.org" };
const joinSessionConfig = { useLegacyMemberEvents: false };
const legacyMembershipData: CallMembershipDataLegacy = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA_legacy",
expires: 60 * 60 * 1000,
membershipID: "bloop",
foci_active: [mockFocus],
};
const expiredLegacyMembershipData: CallMembershipDataLegacy = {
...legacyMembershipData,
device_id: "AAAAAAA_legacy_expired",
expires: 0,
};
const sessionMembershipData: SessionMembershipData = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA_session",
focus_active: mockFocus,
foci_preferred: [mockFocus],
};
function testSession(
membershipData: CallMembershipData[] | SessionMembershipData,
shouldUseLegacy: boolean,
): void {
sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData));
const makeNewLegacyMembershipsMock = jest.spyOn(sess as any, "makeNewLegacyMemberships");
const makeNewMembershipMock = jest.spyOn(sess as any, "makeNewMembership");
sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig);
expect(makeNewLegacyMembershipsMock).toHaveBeenCalledTimes(shouldUseLegacy ? 1 : 0);
expect(makeNewMembershipMock).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1);
}
it("uses legacy events if there are any active legacy calls", () => {
testSession([expiredLegacyMembershipData, legacyMembershipData, sessionMembershipData], true);
});
it('uses legacy events if a non-legacy call is in a "memberships" array', () => {
testSession([sessionMembershipData], true);
});
it("uses non-legacy events if all legacy calls are expired", () => {
testSession([expiredLegacyMembershipData], false);
});
it("uses non-legacy events if there are only non-legacy calls", () => {
testSession(sessionMembershipData, false);
});
});
describe("getOldestMembership", () => {
it("returns the oldest membership event", () => {
const mockRoom = makeMockRoom([
@@ -340,9 +427,20 @@ describe("MatrixRTCSession", () => {
// definitely should have renewed by 1 second before the expiry!
const timeElapsed = 60 * 60 * 1000 - 1000;
mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!.getStateEvents = jest
.fn()
.mockReturnValue(mockRTCEvent(eventContent.memberships, mockRoom.roomId, timeElapsed));
const event = mockRTCEvent(eventContent.memberships, mockRoom.roomId, timeElapsed);
const getState = mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
getState.getStateEvents = jest.fn().mockReturnValue(event);
getState.events = new Map([
[
event.getType(),
{
size: () => true,
has: (_stateKey: string) => true,
get: (_stateKey: string) => event,
values: () => [event],
} as unknown as Map<string, MatrixEvent>,
],
]);
const eventReSentPromise = new Promise<Record<string, any>>((r) => {
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {

View File

@@ -15,13 +15,15 @@ limitations under the License.
*/
import { EventType, MatrixEvent, Room } from "../../../src";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import { CallMembershipData, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
import { randomString } from "../../../src/randomstring";
export function makeMockRoom(memberships: CallMembershipData[], localAge: number | null = null): Room {
type MembershipData = CallMembershipData[] | SessionMembershipData;
export function makeMockRoom(membershipData: MembershipData, localAge: number | null = null): Room {
const roomId = randomString(8);
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
const roomState = makeMockRoomState(memberships, roomId, localAge);
const roomState = makeMockRoomState(membershipData, roomId, localAge);
return {
roomId: roomId,
hasMembershipState: jest.fn().mockReturnValue(true),
@@ -31,8 +33,8 @@ export function makeMockRoom(memberships: CallMembershipData[], localAge: number
} as unknown as Room;
}
export function makeMockRoomState(memberships: CallMembershipData[], roomId: string, localAge: number | null = null) {
const event = mockRTCEvent(memberships, roomId, localAge);
export function makeMockRoomState(membershipData: MembershipData, roomId: string, localAge: number | null = null) {
const event = mockRTCEvent(membershipData, roomId, localAge);
return {
on: jest.fn(),
off: jest.fn(),
@@ -40,15 +42,30 @@ export function makeMockRoomState(memberships: CallMembershipData[], roomId: str
if (stateKey !== undefined) return event;
return [event];
},
events: new Map([
[
event.getType(),
{
size: () => true,
has: (_stateKey: string) => true,
get: (_stateKey: string) => event,
values: () => [event],
},
],
]),
};
}
export function mockRTCEvent(memberships: CallMembershipData[], roomId: string, localAge: number | null): MatrixEvent {
export function mockRTCEvent(membershipData: MembershipData, roomId: string, localAge: number | null): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({
memberships: memberships,
}),
getContent: jest.fn().mockReturnValue(
!Array.isArray(membershipData)
? membershipData
: {
memberships: membershipData,
},
),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
localTimestamp: Date.now() - (localAge ?? 10),

View File

@@ -823,11 +823,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
const localDeviceId = this.client.getDeviceId();
if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!");
const myCallMemberEvent = roomState.getStateEvents(EventType.GroupCallMemberPrefix, localUserId) ?? undefined;
const content = myCallMemberEvent?.getContent() ?? {};
const legacy = "memberships" in content || this.useLegacyMemberEvents;
const callMemberEvents = roomState.events.get(EventType.GroupCallMemberPrefix);
const legacy =
!!this.useLegacyMemberEvents ||
(callMemberEvents?.size && this.stateEventsContainOngoingLegacySession(callMemberEvents));
let newContent: {} | ExperimentalGroupCallRoomMemberState | SessionMembershipData = {};
if (legacy) {
const myCallMemberEvent = callMemberEvents?.get(localUserId);
const content = myCallMemberEvent?.getContent() ?? {};
let myPrevMembership: CallMembership | undefined;
// We know its CallMembershipDataLegacy
const memberships: CallMembershipDataLegacy[] = Array.isArray(content["memberships"])
@@ -866,7 +869,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.room.roomId,
EventType.GroupCallMemberPrefix,
newContent,
this.useLegacyMemberEvents ? localUserId : `${localUserId}_${localDeviceId}`,
legacy ? localUserId : `${localUserId}_${localDeviceId}`,
);
logger.info(`Sent updated call member event.`);
@@ -882,6 +885,20 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
}
private stateEventsContainOngoingLegacySession(callMemberEvents: Map<string, MatrixEvent>): boolean {
for (const callMemberEvent of callMemberEvents.values()) {
const content = callMemberEvent.getContent();
if (Array.isArray(content["memberships"])) {
for (const membership of content.memberships) {
if (!new CallMembership(callMemberEvent, membership).isExpired()) {
return true;
}
}
}
}
return false;
}
private onRotateKeyTimeout = (): void => {
if (!this.manageMediaKeys) return;