small refactor to make it testable.
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
exampleTransport,
|
exampleTransport,
|
||||||
|
mockComputeLivekitParticipantIdentity$,
|
||||||
mockMatrixRoomMember,
|
mockMatrixRoomMember,
|
||||||
mockRtcMembership,
|
mockRtcMembership,
|
||||||
} from "../utils/test";
|
} from "../utils/test";
|
||||||
@@ -47,6 +48,13 @@ vitest.mock("../rtcSessionHelpers", async (importOriginal) => ({
|
|||||||
...(await importOriginal()),
|
...(await importOriginal()),
|
||||||
makeTransport: (): [LivekitTransport] => [exampleTransport],
|
makeTransport: (): [LivekitTransport] => [exampleTransport],
|
||||||
}));
|
}));
|
||||||
|
vitest.mock(
|
||||||
|
import("../state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts"),
|
||||||
|
async (importOriginal) => ({
|
||||||
|
...(await importOriginal()),
|
||||||
|
computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vitest.clearAllMocks();
|
vitest.clearAllMocks();
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import { BrowserRouter } from "react-router-dom";
|
|||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
import { RoomContext, useLocalParticipant } from "@livekit/components-react";
|
import { RoomContext, useLocalParticipant } from "@livekit/components-react";
|
||||||
|
|
||||||
import { InCallView } from "./InCallView";
|
|
||||||
import {
|
import {
|
||||||
|
mockComputeLivekitParticipantIdentity$,
|
||||||
mockLivekitRoom,
|
mockLivekitRoom,
|
||||||
mockLocalParticipant,
|
mockLocalParticipant,
|
||||||
mockMatrixRoom,
|
mockMatrixRoom,
|
||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
mockRtcMembership,
|
mockRtcMembership,
|
||||||
type MockRTCSession,
|
type MockRTCSession,
|
||||||
} from "../utils/test";
|
} from "../utils/test";
|
||||||
|
import { InCallView } from "./InCallView";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
|
||||||
import { alice, local } from "../utils/test-fixtures";
|
import { alice, local } from "../utils/test-fixtures";
|
||||||
@@ -61,6 +62,13 @@ vi.mock("../livekit/MatrixAudioRenderer");
|
|||||||
vi.mock("react-use-measure", () => ({
|
vi.mock("react-use-measure", () => ({
|
||||||
default: (): [() => void, object] => [(): void => {}, {}],
|
default: (): [() => void, object] => [(): void => {}, {}],
|
||||||
}));
|
}));
|
||||||
|
vi.mock(
|
||||||
|
import("../state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts"),
|
||||||
|
async (importOriginal) => ({
|
||||||
|
...(await importOriginal()),
|
||||||
|
computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC");
|
const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC");
|
||||||
const localParticipant = mockLocalParticipant({
|
const localParticipant = mockLocalParticipant({
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
mockRtcMembership,
|
mockRtcMembership,
|
||||||
testScope,
|
testScope,
|
||||||
exampleTransport,
|
exampleTransport,
|
||||||
|
mockComputeLivekitParticipantIdentity$,
|
||||||
} from "../../utils/test.ts";
|
} from "../../utils/test.ts";
|
||||||
import { E2eeType } from "../../e2ee/e2eeType.ts";
|
import { E2eeType } from "../../e2ee/e2eeType.ts";
|
||||||
import {
|
import {
|
||||||
@@ -77,11 +78,22 @@ vi.mock("../e2ee/matrixKeyProvider");
|
|||||||
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
||||||
vi.mock("../UrlParams", () => ({ getUrlParams }));
|
vi.mock("../UrlParams", () => ({ getUrlParams }));
|
||||||
|
|
||||||
vi.mock("../rtcSessionHelpers", async (importOriginal) => ({
|
vi.mock(
|
||||||
...(await importOriginal()),
|
"../state/CallViewModel/localMember/localTransport",
|
||||||
makeTransport: async (): Promise<LivekitTransport> =>
|
async (importOriginal) => ({
|
||||||
Promise.resolve(exampleTransport),
|
...(await importOriginal()),
|
||||||
}));
|
makeTransport: async (): Promise<LivekitTransport> =>
|
||||||
|
Promise.resolve(exampleTransport),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.mock(
|
||||||
|
import("./remoteMembers/LivekitParticipantIdentity.ts"),
|
||||||
|
async (importOriginal) => ({
|
||||||
|
...(await importOriginal()),
|
||||||
|
computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const yesNo = {
|
const yesNo = {
|
||||||
y: true,
|
y: true,
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { encodeUnpaddedBase64Url } from "matrix-js-sdk";
|
||||||
|
import { sha256 } from "matrix-js-sdk/lib/digest";
|
||||||
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
|
import { from, type Observable } from "rxjs";
|
||||||
|
|
||||||
|
const livekitParticipantIdentityCache = new Map<string, string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The string that is computed based on the membership and used for the computing the hash.
|
||||||
|
* `${userId}:${deviceId}:${membershipID}`
|
||||||
|
* as the direct imput for: await sha256(input)
|
||||||
|
*/
|
||||||
|
export const livekitIdentityInput = ({
|
||||||
|
userId,
|
||||||
|
deviceId,
|
||||||
|
memberId,
|
||||||
|
}: CallMembershipIdentityParts): string => `${userId}|${deviceId}|${memberId}`;
|
||||||
|
|
||||||
|
export function computeLivekitParticipantIdentity$(
|
||||||
|
membership: CallMembershipIdentityParts,
|
||||||
|
kind: "rtc" | "session",
|
||||||
|
): Observable<string> {
|
||||||
|
const compute = async (): Promise<string> => {
|
||||||
|
switch (kind) {
|
||||||
|
case "rtc": {
|
||||||
|
const input = livekitIdentityInput(membership);
|
||||||
|
if (livekitParticipantIdentityCache.size > 400)
|
||||||
|
// prevent memory leaks in a stupid/simple way
|
||||||
|
livekitParticipantIdentityCache.clear();
|
||||||
|
// TODO use non deprecated memberId
|
||||||
|
if (livekitParticipantIdentityCache.has(input))
|
||||||
|
return livekitParticipantIdentityCache.get(input)!;
|
||||||
|
else {
|
||||||
|
const hashBuffer = await sha256(input);
|
||||||
|
const hashedString = encodeUnpaddedBase64Url(hashBuffer);
|
||||||
|
livekitParticipantIdentityCache.set(input, hashedString);
|
||||||
|
return hashedString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "session":
|
||||||
|
default:
|
||||||
|
return `${membership.userId}:${membership.deviceId}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return from(compute());
|
||||||
|
}
|
||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
flushPromises,
|
flushPromises,
|
||||||
mockCallMembership,
|
mockCallMembership,
|
||||||
mockRemoteParticipant,
|
mockRemoteParticipant,
|
||||||
withTestScheduler,
|
|
||||||
} from "../../../utils/test.ts";
|
} from "../../../utils/test.ts";
|
||||||
import { type Connection } from "./Connection.ts";
|
import { type Connection } from "./Connection.ts";
|
||||||
import { constant } from "../../Behavior.ts";
|
import { constant } from "../../Behavior.ts";
|
||||||
|
|||||||
@@ -15,15 +15,13 @@ import {
|
|||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { combineLatest, filter, map, switchMap } from "rxjs";
|
import { combineLatest, filter, map, switchMap } from "rxjs";
|
||||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
import { sha256 } from "matrix-js-sdk/lib/digest";
|
|
||||||
import { encodeUnpaddedBase64Url } from "matrix-js-sdk";
|
|
||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
|
||||||
|
|
||||||
import { type Behavior } from "../../Behavior";
|
import { type Behavior } from "../../Behavior";
|
||||||
import { type IConnectionManager } from "./ConnectionManager";
|
import { type IConnectionManager } from "./ConnectionManager";
|
||||||
import { Epoch, type ObservableScope } from "../../ObservableScope";
|
import { Epoch, type ObservableScope } from "../../ObservableScope";
|
||||||
import { type Connection } from "./Connection";
|
import { type Connection } from "./Connection";
|
||||||
import { generateItemsWithEpoch } from "../../../utils/observable";
|
import { generateItemsWithEpoch } from "../../../utils/observable";
|
||||||
|
import { computeLivekitParticipantIdentity$ } from "./LivekitParticipantIdentity";
|
||||||
|
|
||||||
const logger = rootLogger.getChild("[MatrixLivekitMembers]");
|
const logger = rootLogger.getChild("[MatrixLivekitMembers]");
|
||||||
|
|
||||||
@@ -73,25 +71,28 @@ export function createMatrixLivekitMembers$({
|
|||||||
*/
|
*/
|
||||||
const membershipsWithTransportAndLivekitIdentity$ =
|
const membershipsWithTransportAndLivekitIdentity$ =
|
||||||
membershipsWithTransport$.pipe(
|
membershipsWithTransport$.pipe(
|
||||||
switchMap(async (membershipsWithTransport) => {
|
switchMap((membershipsWithTransport) => {
|
||||||
const { value, epoch } = membershipsWithTransport;
|
const { value, epoch } = membershipsWithTransport;
|
||||||
const membershipsWithTransportAndLkIdentityPromises = value.map(
|
const membershipsWithTransportAndLkIdentityPromises = value.map(
|
||||||
async (obj) => {
|
(obj) => {
|
||||||
return computeLivekitParticipantIdentity(
|
return computeLivekitParticipantIdentity$(
|
||||||
obj.membership,
|
obj.membership,
|
||||||
obj.membership.kind,
|
obj.membership.kind,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const identities = await Promise.all(
|
return combineLatest(
|
||||||
membershipsWithTransportAndLkIdentityPromises,
|
membershipsWithTransportAndLkIdentityPromises,
|
||||||
|
).pipe(
|
||||||
|
map((identities) => {
|
||||||
|
const membershipsWithTransportAndLkIdentity = value.map(
|
||||||
|
({ transport, membership }, index) => {
|
||||||
|
return { transport, membership, identity: identities[index] };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return new Epoch(membershipsWithTransportAndLkIdentity, epoch);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const membershipsWithTransportAndLkIdentity = value.map(
|
|
||||||
({ transport, membership }, index) => {
|
|
||||||
return { transport, membership, identity: identities[index] };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return new Epoch(membershipsWithTransportAndLkIdentity, epoch);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -164,42 +165,3 @@ export function areLivekitTransportsEqual(
|
|||||||
if (!t1 && !t2) return true;
|
if (!t1 && !t2) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const livekitParticipantIdentityCache = new Map<string, string>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The string that is computed based on the membership and used for the computing the hash.
|
|
||||||
* `${userId}:${deviceId}:${membershipID}`
|
|
||||||
* as the direct imput for: await sha256(input)
|
|
||||||
*/
|
|
||||||
export const livekitIdentityInput = ({
|
|
||||||
userId,
|
|
||||||
deviceId,
|
|
||||||
memberId,
|
|
||||||
}: CallMembershipIdentityParts): string => `${userId}|${deviceId}|${memberId}`;
|
|
||||||
|
|
||||||
export async function computeLivekitParticipantIdentity(
|
|
||||||
membership: CallMembershipIdentityParts,
|
|
||||||
kind: "rtc" | "session",
|
|
||||||
): Promise<string> {
|
|
||||||
switch (kind) {
|
|
||||||
case "rtc": {
|
|
||||||
const input = livekitIdentityInput(membership);
|
|
||||||
if (livekitParticipantIdentityCache.size > 400)
|
|
||||||
// prevent memory leaks in a stupid/simple way
|
|
||||||
livekitParticipantIdentityCache.clear();
|
|
||||||
// TODO use non deprecated memberId
|
|
||||||
if (livekitParticipantIdentityCache.has(input))
|
|
||||||
return livekitParticipantIdentityCache.get(input)!;
|
|
||||||
else {
|
|
||||||
const hashBuffer = await sha256(input);
|
|
||||||
const hashedString = encodeUnpaddedBase64Url(hashBuffer);
|
|
||||||
livekitParticipantIdentityCache.set(input, hashedString);
|
|
||||||
return hashedString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "session":
|
|
||||||
default:
|
|
||||||
return `${membership.userId}:${membership.deviceId}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { ECConnectionFactory } from "./ConnectionFactory.ts";
|
|||||||
import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
|
import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
|
||||||
import {
|
import {
|
||||||
mockCallMembership,
|
mockCallMembership,
|
||||||
|
mockComputeLivekitParticipantIdentity$,
|
||||||
mockMediaDevices,
|
mockMediaDevices,
|
||||||
withTestScheduler,
|
withTestScheduler,
|
||||||
} from "../../../utils/test.ts";
|
} from "../../../utils/test.ts";
|
||||||
@@ -43,6 +44,11 @@ let lkRoomFactory: () => LivekitRoom;
|
|||||||
|
|
||||||
const createdMockLivekitRooms: Map<string, LivekitRoom> = new Map();
|
const createdMockLivekitRooms: Map<string, LivekitRoom> = new Map();
|
||||||
|
|
||||||
|
vi.mock(import("./LivekitParticipantIdentity.ts"), async (importOriginal) => ({
|
||||||
|
...(await importOriginal()),
|
||||||
|
computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$,
|
||||||
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
testScope = new ObservableScope();
|
testScope = new ObservableScope();
|
||||||
mockClient = {
|
mockClient = {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
vitest,
|
vitest,
|
||||||
} from "vitest";
|
} from "vitest";
|
||||||
import {
|
import {
|
||||||
|
encodeUnpaddedBase64,
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
type Room as MatrixRoom,
|
type Room as MatrixRoom,
|
||||||
type Room,
|
type Room,
|
||||||
@@ -43,13 +44,14 @@ import {
|
|||||||
type Room as LivekitRoom,
|
type Room as LivekitRoom,
|
||||||
Track,
|
Track,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { randomUUID } from "crypto";
|
import { createHash, randomUUID } from "crypto";
|
||||||
import { type TrackReference } from "@livekit/components-core";
|
import { type TrackReference } from "@livekit/components-core";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import {
|
import {
|
||||||
type KeyTransportEvents,
|
type KeyTransportEvents,
|
||||||
type KeyTransportEventsHandlerMap,
|
type KeyTransportEventsHandlerMap,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc/IKeyTransport";
|
} from "matrix-js-sdk/lib/matrixrtc/IKeyTransport";
|
||||||
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LocalUserMediaViewModel,
|
LocalUserMediaViewModel,
|
||||||
@@ -522,3 +524,27 @@ export function mockMuteStates(
|
|||||||
const observableScope = new ObservableScope();
|
const observableScope = new ObservableScope();
|
||||||
return new MuteStates(observableScope, mockMediaDevices({}), joined$);
|
return new MuteStates(observableScope, mockMediaDevices({}), joined$);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mockComputeLivekitParticipantIdentity$ = (
|
||||||
|
membership: CallMembershipIdentityParts,
|
||||||
|
kind: "rtc" | "session",
|
||||||
|
): Observable<string> => {
|
||||||
|
function sha256(commitmentStr: string): string {
|
||||||
|
return encodeUnpaddedBase64(
|
||||||
|
createHash("sha256").update(commitmentStr, "utf8").digest(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let hash;
|
||||||
|
switch (kind) {
|
||||||
|
case "rtc": {
|
||||||
|
hash = sha256(
|
||||||
|
`${membership.userId}|${membership.deviceId}|${membership.memberId}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "session":
|
||||||
|
default:
|
||||||
|
hash = `${membership.userId}:${membership.deviceId}`;
|
||||||
|
}
|
||||||
|
return of(hash);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user