Ensure correct room version is used and permissions are appropriately sert when creating rooms (#31464)
* Check default PL when setting a new PL in createRoom * Drop custom PL setting for video rooms * lint files * Add room version test * Cleanup test * fix import
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
@@ -39,10 +40,10 @@ import { findDMForUser } from "./utils/dm/findDMForUser";
|
||||
import { privateShouldBeEncrypted } from "./utils/rooms";
|
||||
import { shouldForceDisableEncryption } from "./utils/crypto/shouldForceDisableEncryption";
|
||||
import { waitForMember } from "./utils/membership";
|
||||
import { PreferredRoomVersions } from "./utils/PreferredRoomVersions";
|
||||
import { doesRoomVersionSupport, PreferredRoomVersions } from "./utils/PreferredRoomVersions";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto";
|
||||
import { ElementCallEventType, ElementCallMemberEventType } from "./call-types";
|
||||
import { ElementCallMemberEventType } from "./call-types";
|
||||
|
||||
// we define a number of interfaces which take their names from the js-sdk
|
||||
/* eslint-disable camelcase */
|
||||
@@ -159,32 +160,19 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
|
||||
};
|
||||
|
||||
// Video rooms require custom power levels
|
||||
if (opts.roomType === RoomType.ElementVideo) {
|
||||
if (opts.roomType === RoomType.ElementVideo || opts.roomType === RoomType.UnstableCall) {
|
||||
createOpts.power_level_content_override = {
|
||||
events: {
|
||||
...DEFAULT_EVENT_POWER_LEVELS,
|
||||
// Allow all users to send call membership updates
|
||||
[JitsiCall.MEMBER_EVENT_TYPE]: 0,
|
||||
// Make widgets immutable, even to admins
|
||||
"im.vector.modular.widgets": 200,
|
||||
},
|
||||
users: {
|
||||
// Temporarily give ourselves the power to set up a widget
|
||||
[client.getSafeUserId()]: 200,
|
||||
},
|
||||
};
|
||||
} else if (opts.roomType === RoomType.UnstableCall) {
|
||||
createOpts.power_level_content_override = {
|
||||
events: {
|
||||
...DEFAULT_EVENT_POWER_LEVELS,
|
||||
// Allow all users to send call membership updates
|
||||
[ElementCallMemberEventType.name]: 0,
|
||||
// Make calls immutable, even to admins
|
||||
[ElementCallEventType.name]: 200,
|
||||
},
|
||||
users: {
|
||||
// Temporarily give ourselves the power to set up a call
|
||||
[client.getSafeUserId()]: 200,
|
||||
[opts.roomType === RoomType.ElementVideo
|
||||
? JitsiCall.MEMBER_EVENT_TYPE
|
||||
: ElementCallMemberEventType.name]: 0,
|
||||
// Ensure all but admins can't change widgets
|
||||
// A previous version of the code prevented even administrators
|
||||
// from changing this, but this is not possible now that room creators
|
||||
// have an immutable power level
|
||||
["im.vector.modular.widgets"]: 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -194,8 +182,6 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
|
||||
...DEFAULT_EVENT_POWER_LEVELS,
|
||||
// It should always (including non video rooms) be possible to join a group call.
|
||||
[ElementCallMemberEventType.name]: 0,
|
||||
// Make sure only admins can enable it (DEPRECATED)
|
||||
[ElementCallEventType.name]: 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -230,7 +216,12 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.joinRule === JoinRule.Knock) {
|
||||
const defaultRoomVersion = (await client.getCapabilities())["m.room_versions"]?.default ?? "1";
|
||||
|
||||
if (
|
||||
opts.joinRule === JoinRule.Knock &&
|
||||
!doesRoomVersionSupport(defaultRoomVersion, PreferredRoomVersions.KnockRooms)
|
||||
) {
|
||||
createOpts.room_version = PreferredRoomVersions.KnockRooms;
|
||||
}
|
||||
|
||||
@@ -238,7 +229,9 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
|
||||
createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
|
||||
|
||||
if (opts.joinRule === JoinRule.Restricted) {
|
||||
createOpts.room_version = PreferredRoomVersions.RestrictedRooms;
|
||||
if (!doesRoomVersionSupport(defaultRoomVersion, PreferredRoomVersions.KnockRooms)) {
|
||||
createOpts.room_version = PreferredRoomVersions.RestrictedRooms;
|
||||
}
|
||||
|
||||
createOpts.initial_state.push({
|
||||
type: EventType.RoomJoinRules,
|
||||
@@ -354,15 +347,9 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
|
||||
if (opts.roomType === RoomType.ElementVideo) {
|
||||
// Set up this video room with a Jitsi call
|
||||
await JitsiCall.create(await room);
|
||||
|
||||
// Reset our power level back to admin so that the widget becomes immutable
|
||||
await client.setPowerLevel(roomId, client.getUserId()!, 100);
|
||||
} else if (opts.roomType === RoomType.UnstableCall) {
|
||||
// Set up this video room with an Element call
|
||||
ElementCall.create(await room);
|
||||
|
||||
// Reset our power level back to admin so that the call becomes immutable
|
||||
await client.setPowerLevel(roomId, client.getUserId()!, 100);
|
||||
}
|
||||
})
|
||||
.then(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -7,19 +8,33 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked, type Mocked } from "jest-mock";
|
||||
import { type MatrixClient, type Device, Preset, RoomType } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
type MatrixClient,
|
||||
type Device,
|
||||
Preset,
|
||||
RoomType,
|
||||
JoinRule,
|
||||
RoomVersionStability,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||
|
||||
import { stubClient, setupAsyncStoreWithClient, mockPlatformPeg, getMockClientWithEventEmitter } from "../test-utils";
|
||||
import {
|
||||
stubClient,
|
||||
setupAsyncStoreWithClient,
|
||||
mockPlatformPeg,
|
||||
getMockClientWithEventEmitter,
|
||||
mkRoom,
|
||||
} from "../test-utils";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import WidgetStore from "../../src/stores/WidgetStore";
|
||||
import WidgetUtils from "../../src/utils/WidgetUtils";
|
||||
import { JitsiCall, ElementCall } from "../../src/models/Call";
|
||||
import createRoom, { checkUserIsAllowedToChangeEncryption, canEncryptToAllUsers } from "../../src/createRoom";
|
||||
import SettingsStore from "../../src/settings/SettingsStore";
|
||||
import { ElementCallEventType, ElementCallMemberEventType } from "../../src/call-types";
|
||||
import { ElementCallMemberEventType } from "../../src/call-types";
|
||||
import DMRoomMap from "../../src/utils/DMRoomMap";
|
||||
import { PreferredRoomVersions } from "../../src/utils/PreferredRoomVersions";
|
||||
|
||||
describe("createRoom", () => {
|
||||
mockPlatformPeg();
|
||||
@@ -103,14 +118,12 @@ describe("createRoom", () => {
|
||||
jest.spyOn(WidgetUtils, "waitForRoomWidget").mockResolvedValue();
|
||||
const createCallSpy = jest.spyOn(JitsiCall, "create");
|
||||
|
||||
const userId = client.getUserId()!;
|
||||
const roomId = await createRoom(client, { roomType: RoomType.ElementVideo });
|
||||
await createRoom(client, { roomType: RoomType.ElementVideo });
|
||||
|
||||
const [
|
||||
[
|
||||
{
|
||||
power_level_content_override: {
|
||||
users: { [userId]: userPower },
|
||||
events: {
|
||||
"im.vector.modular.widgets": widgetPower,
|
||||
[JitsiCall.MEMBER_EVENT_TYPE]: callMemberPower,
|
||||
@@ -120,44 +133,30 @@ describe("createRoom", () => {
|
||||
],
|
||||
] = client.createRoom.mock.calls as any; // no good type
|
||||
|
||||
// We should have had enough power to be able to set up the widget
|
||||
expect(userPower).toBeGreaterThanOrEqual(widgetPower);
|
||||
// and should have actually set it up
|
||||
expect(createCallSpy).toHaveBeenCalled();
|
||||
|
||||
// All members should be able to update their connected devices
|
||||
expect(callMemberPower).toEqual(0);
|
||||
// widget should be immutable for admins
|
||||
expect(widgetPower).toBeGreaterThan(100);
|
||||
// and we should have been reset back to admin
|
||||
expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, userId, 100);
|
||||
expect(widgetPower).toEqual(100);
|
||||
});
|
||||
|
||||
it("sets up Element video rooms correctly", async () => {
|
||||
const userId = client.getUserId()!;
|
||||
const createCallSpy = jest.spyOn(ElementCall, "create");
|
||||
const callMembershipSpy = jest.spyOn(MatrixRTCSession, "callMembershipsForRoom");
|
||||
callMembershipSpy.mockReturnValue([]);
|
||||
|
||||
const roomId = await createRoom(client, { roomType: RoomType.UnstableCall });
|
||||
await createRoom(client, { roomType: RoomType.UnstableCall });
|
||||
|
||||
const userPower = client.createRoom.mock.calls[0][0].power_level_content_override?.users?.[userId];
|
||||
const callPower =
|
||||
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallEventType.name];
|
||||
const callMemberPower =
|
||||
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallMemberEventType.name];
|
||||
|
||||
// We should have had enough power to be able to set up the call
|
||||
expect(userPower).toBeGreaterThanOrEqual(callPower!);
|
||||
// and should have actually set it up
|
||||
expect(createCallSpy).toHaveBeenCalled();
|
||||
|
||||
// All members should be able to update their connected devices
|
||||
expect(callMemberPower).toEqual(0);
|
||||
// call should be immutable for admins
|
||||
expect(callPower).toBeGreaterThan(100);
|
||||
// and we should have been reset back to admin
|
||||
expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, userId, 100);
|
||||
});
|
||||
|
||||
it("doesn't create calls in non-video-rooms", async () => {
|
||||
@@ -177,12 +176,9 @@ describe("createRoom", () => {
|
||||
|
||||
await createRoom(client, {});
|
||||
|
||||
const callPower =
|
||||
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallEventType.name];
|
||||
const callMemberPower =
|
||||
client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallMemberEventType.name];
|
||||
|
||||
expect(callPower).toBe(100);
|
||||
expect(callMemberPower).toBe(0);
|
||||
});
|
||||
|
||||
@@ -212,6 +208,80 @@ describe("createRoom", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe("room versions", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it("should use the correct room version for knocking when default does not support it", async () => {
|
||||
client.getCapabilities.mockResolvedValue({
|
||||
"m.room_versions": {
|
||||
default: "1",
|
||||
available: {
|
||||
[PreferredRoomVersions.KnockRooms]: RoomVersionStability.Stable,
|
||||
"1": RoomVersionStability.Stable,
|
||||
},
|
||||
},
|
||||
});
|
||||
await createRoom(client, { joinRule: JoinRule.Knock });
|
||||
expect(client.createRoom).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
room_version: PreferredRoomVersions.KnockRooms,
|
||||
}),
|
||||
);
|
||||
});
|
||||
it("should use the default room version for knocking when default supports it", async () => {
|
||||
client.getCapabilities.mockResolvedValue({
|
||||
"m.room_versions": {
|
||||
default: "12",
|
||||
available: {
|
||||
[PreferredRoomVersions.KnockRooms]: RoomVersionStability.Stable,
|
||||
"12": RoomVersionStability.Stable,
|
||||
},
|
||||
},
|
||||
});
|
||||
await createRoom(client, { joinRule: JoinRule.Knock });
|
||||
expect(client.createRoom).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
room_version: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
it("should use the correct room version for restricted join rules when default does not support it", async () => {
|
||||
client.getCapabilities.mockResolvedValue({
|
||||
"m.room_versions": {
|
||||
default: "1",
|
||||
available: {
|
||||
[PreferredRoomVersions.RestrictedRooms]: RoomVersionStability.Stable,
|
||||
"1": RoomVersionStability.Stable,
|
||||
},
|
||||
},
|
||||
});
|
||||
await createRoom(client, { parentSpace: mkRoom(client, "!parent"), joinRule: JoinRule.Restricted });
|
||||
expect(client.createRoom).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
room_version: PreferredRoomVersions.RestrictedRooms,
|
||||
}),
|
||||
);
|
||||
});
|
||||
it("should use the default room version for restricted join rules when default supports it", async () => {
|
||||
client.getCapabilities.mockResolvedValue({
|
||||
"m.room_versions": {
|
||||
default: "12",
|
||||
available: {
|
||||
[PreferredRoomVersions.RestrictedRooms]: RoomVersionStability.Stable,
|
||||
"12": RoomVersionStability.Stable,
|
||||
},
|
||||
},
|
||||
});
|
||||
await createRoom(client, { parentSpace: mkRoom(client, "!parent"), joinRule: JoinRule.Restricted });
|
||||
expect(client.createRoom).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
room_version: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("canEncryptToAllUsers", () => {
|
||||
|
||||
Reference in New Issue
Block a user