You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-07 23:02:56 +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:
committed by
GitHub
parent
238eea0ef5
commit
6a15e8f1a0
@@ -16,7 +16,11 @@ limitations under the License.
|
|||||||
|
|
||||||
import { EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
|
import { EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
|
||||||
import { KnownMembership } from "../../../src/@types/membership";
|
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 { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
||||||
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
||||||
import { randomString } from "../../../src/randomstring";
|
import { randomString } from "../../../src/randomstring";
|
||||||
@@ -99,22 +103,33 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("safely ignores events with no memberships section", () => {
|
it("safely ignores events with no memberships section", () => {
|
||||||
const mockRoom = {
|
const roomId = randomString(8);
|
||||||
...makeMockRoom([]),
|
const event = {
|
||||||
roomId: randomString(8),
|
|
||||||
getLiveTimeline: jest.fn().mockReturnValue({
|
|
||||||
getState: jest.fn().mockReturnValue({
|
|
||||||
on: jest.fn(),
|
|
||||||
off: jest.fn(),
|
|
||||||
getStateEvents: (_type: string, _stateKey: string) => [
|
|
||||||
{
|
|
||||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||||
getContent: jest.fn().mockReturnValue({}),
|
getContent: jest.fn().mockReturnValue({}),
|
||||||
getSender: jest.fn().mockReturnValue("@mock:user.example"),
|
getSender: jest.fn().mockReturnValue("@mock:user.example"),
|
||||||
getTs: jest.fn().mockReturnValue(1000),
|
getTs: jest.fn().mockReturnValue(1000),
|
||||||
getLocalAge: jest.fn().mockReturnValue(0),
|
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", () => {
|
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 = {
|
const mockRoom = {
|
||||||
...makeMockRoom([]),
|
...makeMockRoom([]),
|
||||||
roomId: randomString(8),
|
roomId,
|
||||||
getLiveTimeline: jest.fn().mockReturnValue({
|
getLiveTimeline: jest.fn().mockReturnValue({
|
||||||
getState: jest.fn().mockReturnValue({
|
getState: jest.fn().mockReturnValue({
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
off: 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),
|
size: () => true,
|
||||||
getContent: jest.fn().mockReturnValue({ memberships: "i am a fish" }),
|
has: (_stateKey: string) => true,
|
||||||
getSender: jest.fn().mockReturnValue("@mock:user.example"),
|
get: (_stateKey: string) => event,
|
||||||
getTs: jest.fn().mockReturnValue(1000),
|
values: () => [event],
|
||||||
getLocalAge: jest.fn().mockReturnValue(0),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
]),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -186,6 +212,67 @@ describe("MatrixRTCSession", () => {
|
|||||||
expect(sess.memberships).toHaveLength(0);
|
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", () => {
|
describe("getOldestMembership", () => {
|
||||||
it("returns the oldest membership event", () => {
|
it("returns the oldest membership event", () => {
|
||||||
const mockRoom = makeMockRoom([
|
const mockRoom = makeMockRoom([
|
||||||
@@ -340,9 +427,20 @@ describe("MatrixRTCSession", () => {
|
|||||||
|
|
||||||
// definitely should have renewed by 1 second before the expiry!
|
// definitely should have renewed by 1 second before the expiry!
|
||||||
const timeElapsed = 60 * 60 * 1000 - 1000;
|
const timeElapsed = 60 * 60 * 1000 - 1000;
|
||||||
mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!.getStateEvents = jest
|
const event = mockRTCEvent(eventContent.memberships, mockRoom.roomId, timeElapsed);
|
||||||
.fn()
|
const getState = mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||||
.mockReturnValue(mockRTCEvent(eventContent.memberships, mockRoom.roomId, timeElapsed));
|
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) => {
|
const eventReSentPromise = new Promise<Record<string, any>>((r) => {
|
||||||
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {
|
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {
|
||||||
|
@@ -15,13 +15,15 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventType, MatrixEvent, Room } from "../../../src";
|
import { EventType, MatrixEvent, Room } from "../../../src";
|
||||||
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
|
import { CallMembershipData, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||||
import { randomString } from "../../../src/randomstring";
|
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);
|
const roomId = randomString(8);
|
||||||
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
|
// 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 {
|
return {
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
hasMembershipState: jest.fn().mockReturnValue(true),
|
hasMembershipState: jest.fn().mockReturnValue(true),
|
||||||
@@ -31,8 +33,8 @@ export function makeMockRoom(memberships: CallMembershipData[], localAge: number
|
|||||||
} as unknown as Room;
|
} as unknown as Room;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeMockRoomState(memberships: CallMembershipData[], roomId: string, localAge: number | null = null) {
|
export function makeMockRoomState(membershipData: MembershipData, roomId: string, localAge: number | null = null) {
|
||||||
const event = mockRTCEvent(memberships, roomId, localAge);
|
const event = mockRTCEvent(membershipData, roomId, localAge);
|
||||||
return {
|
return {
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
off: jest.fn(),
|
off: jest.fn(),
|
||||||
@@ -40,15 +42,30 @@ export function makeMockRoomState(memberships: CallMembershipData[], roomId: str
|
|||||||
if (stateKey !== undefined) return event;
|
if (stateKey !== undefined) return event;
|
||||||
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 {
|
return {
|
||||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||||
getContent: jest.fn().mockReturnValue({
|
getContent: jest.fn().mockReturnValue(
|
||||||
memberships: memberships,
|
!Array.isArray(membershipData)
|
||||||
}),
|
? membershipData
|
||||||
|
: {
|
||||||
|
memberships: membershipData,
|
||||||
|
},
|
||||||
|
),
|
||||||
getSender: jest.fn().mockReturnValue("@mock:user.example"),
|
getSender: jest.fn().mockReturnValue("@mock:user.example"),
|
||||||
getTs: jest.fn().mockReturnValue(1000),
|
getTs: jest.fn().mockReturnValue(1000),
|
||||||
localTimestamp: Date.now() - (localAge ?? 10),
|
localTimestamp: Date.now() - (localAge ?? 10),
|
||||||
|
@@ -823,11 +823,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
const localDeviceId = this.client.getDeviceId();
|
const localDeviceId = this.client.getDeviceId();
|
||||||
if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!");
|
if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!");
|
||||||
|
|
||||||
const myCallMemberEvent = roomState.getStateEvents(EventType.GroupCallMemberPrefix, localUserId) ?? undefined;
|
const callMemberEvents = roomState.events.get(EventType.GroupCallMemberPrefix);
|
||||||
const content = myCallMemberEvent?.getContent() ?? {};
|
const legacy =
|
||||||
const legacy = "memberships" in content || this.useLegacyMemberEvents;
|
!!this.useLegacyMemberEvents ||
|
||||||
|
(callMemberEvents?.size && this.stateEventsContainOngoingLegacySession(callMemberEvents));
|
||||||
let newContent: {} | ExperimentalGroupCallRoomMemberState | SessionMembershipData = {};
|
let newContent: {} | ExperimentalGroupCallRoomMemberState | SessionMembershipData = {};
|
||||||
if (legacy) {
|
if (legacy) {
|
||||||
|
const myCallMemberEvent = callMemberEvents?.get(localUserId);
|
||||||
|
const content = myCallMemberEvent?.getContent() ?? {};
|
||||||
let myPrevMembership: CallMembership | undefined;
|
let myPrevMembership: CallMembership | undefined;
|
||||||
// We know its CallMembershipDataLegacy
|
// We know its CallMembershipDataLegacy
|
||||||
const memberships: CallMembershipDataLegacy[] = Array.isArray(content["memberships"])
|
const memberships: CallMembershipDataLegacy[] = Array.isArray(content["memberships"])
|
||||||
@@ -866,7 +869,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
this.room.roomId,
|
this.room.roomId,
|
||||||
EventType.GroupCallMemberPrefix,
|
EventType.GroupCallMemberPrefix,
|
||||||
newContent,
|
newContent,
|
||||||
this.useLegacyMemberEvents ? localUserId : `${localUserId}_${localDeviceId}`,
|
legacy ? localUserId : `${localUserId}_${localDeviceId}`,
|
||||||
);
|
);
|
||||||
logger.info(`Sent updated call member event.`);
|
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 => {
|
private onRotateKeyTimeout = (): void => {
|
||||||
if (!this.manageMediaKeys) return;
|
if (!this.manageMediaKeys) return;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user