You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-30 04:23:07 +03:00
Early implementation of MSC3089 (file trees)
MSC: https://github.com/matrix-org/matrix-doc/pull/3089 Includes part of MSC3088 (room subtyping): https://github.com/matrix-org/matrix-doc/pull/3088 The NamespacedValue stuff is borrowed from the Extensible Events implementation PR in the react-sdk as a useful thing to put here. When/if the MSCs become stable, we'd convert the values to enums and drop the constants (or keep them for migration purposes, but switch to stable). This flags the whole thing as unstable because it's highly subject to change.
This commit is contained in:
78
spec/unit/NamespacedValue.spec.ts
Normal file
78
spec/unit/NamespacedValue.spec.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {NamespacedValue, UnstableValue} from "../../src/NamespacedValue";
|
||||||
|
|
||||||
|
describe("NamespacedValue", () => {
|
||||||
|
it("should prefer stable over unstable", () => {
|
||||||
|
const ns = new NamespacedValue("stable", "unstable");
|
||||||
|
expect(ns.name).toBe(ns.stable);
|
||||||
|
expect(ns.altName).toBe(ns.unstable);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return unstable if there is no stable", () => {
|
||||||
|
const ns = new NamespacedValue(null, "unstable");
|
||||||
|
expect(ns.name).toBe(ns.unstable);
|
||||||
|
expect(ns.altName).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have a falsey unstable if needed", () => {
|
||||||
|
const ns = new NamespacedValue("stable", null);
|
||||||
|
expect(ns.name).toBe(ns.stable);
|
||||||
|
expect(ns.altName).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match against either stable or unstable", () => {
|
||||||
|
const ns = new NamespacedValue("stable", "unstable");
|
||||||
|
expect(ns.matches("no")).toBe(false);
|
||||||
|
expect(ns.matches(ns.stable)).toBe(true);
|
||||||
|
expect(ns.matches(ns.unstable)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not permit falsey values for both parts", () => {
|
||||||
|
try {
|
||||||
|
new UnstableValue(null, null);
|
||||||
|
// noinspection ExceptionCaughtLocallyJS
|
||||||
|
throw new Error("Failed to fail");
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe("One of stable or unstable values must be supplied");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("UnstableValue", () => {
|
||||||
|
it("should prefer unstable over stable", () => {
|
||||||
|
const ns = new UnstableValue("stable", "unstable");
|
||||||
|
expect(ns.name).toBe(ns.unstable);
|
||||||
|
expect(ns.altName).toBe(ns.stable);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return unstable if there is no stable", () => {
|
||||||
|
const ns = new UnstableValue(null, "unstable");
|
||||||
|
expect(ns.name).toBe(ns.unstable);
|
||||||
|
expect(ns.altName).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not permit falsey unstable values", () => {
|
||||||
|
try {
|
||||||
|
new UnstableValue("stable", null);
|
||||||
|
// noinspection ExceptionCaughtLocallyJS
|
||||||
|
throw new Error("Failed to fail");
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toBe("Unstable value must be supplied");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,16 @@
|
|||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
import { MatrixClient } from "../../src/client";
|
import { MatrixClient } from "../../src/client";
|
||||||
import { Filter } from "../../src/filter";
|
import { Filter } from "../../src/filter";
|
||||||
|
import {DEFAULT_TREE_POWER_LEVELS_TEMPLATE} from "../../src/models/MSC3089TreeSpace";
|
||||||
|
import {
|
||||||
|
EventType,
|
||||||
|
RoomCreateTypeField, RoomType,
|
||||||
|
UNSTABLE_MSC3088_ENABLED,
|
||||||
|
UNSTABLE_MSC3088_PURPOSE,
|
||||||
|
UNSTABLE_MSC3089_TREE_SUBTYPE
|
||||||
|
} from "../../src/@types/event";
|
||||||
|
import {MEGOLM_ALGORITHM} from "../../src/crypto/olmlib";
|
||||||
|
import {MatrixEvent} from "../../src/models/event";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
@ -171,6 +181,160 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should create (unstable) file trees", async () => {
|
||||||
|
const userId = "@test:example.org";
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const roomName = "Test Tree";
|
||||||
|
const mockRoom = {};
|
||||||
|
const fn = jest.fn().mockImplementation((opts) => {
|
||||||
|
expect(opts).toMatchObject({
|
||||||
|
name: roomName,
|
||||||
|
preset: "private_chat",
|
||||||
|
power_level_content_override: {
|
||||||
|
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||||
|
users: {
|
||||||
|
[userId]: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
creation_content: {
|
||||||
|
[RoomCreateTypeField]: RoomType.Space,
|
||||||
|
},
|
||||||
|
initial_state: [
|
||||||
|
{
|
||||||
|
// We use `unstable` to ensure that the code is actually using the right identifier
|
||||||
|
type: UNSTABLE_MSC3088_PURPOSE.unstable,
|
||||||
|
state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.unstable,
|
||||||
|
content: {
|
||||||
|
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: EventType.RoomEncryption,
|
||||||
|
state_key: "",
|
||||||
|
content: {
|
||||||
|
algorithm: MEGOLM_ALGORITHM,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return {room_id: roomId};
|
||||||
|
});
|
||||||
|
client.getUserId = () => userId;
|
||||||
|
client.createRoom = fn;
|
||||||
|
client.getRoom = (getRoomId) => {
|
||||||
|
expect(getRoomId).toEqual(roomId);
|
||||||
|
return mockRoom;
|
||||||
|
};
|
||||||
|
const tree = await client.unstableCreateFileTree(roomName);
|
||||||
|
expect(tree).toBeDefined();
|
||||||
|
expect(tree.roomId).toEqual(roomId);
|
||||||
|
expect(tree.room).toBe(mockRoom);
|
||||||
|
expect(fn.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get (unstable) file trees with valid state", async () => {
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const mockRoom = {
|
||||||
|
currentState: {
|
||||||
|
getStateEvents: (eventType, stateKey) => {
|
||||||
|
if (eventType === EventType.RoomCreate) {
|
||||||
|
expect(stateKey).toEqual("");
|
||||||
|
return new MatrixEvent({
|
||||||
|
content: {
|
||||||
|
[RoomCreateTypeField]: RoomType.Space,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
|
||||||
|
// We use `unstable` to ensure that the code is actually using the right identifier
|
||||||
|
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||||
|
return new MatrixEvent({
|
||||||
|
content: {
|
||||||
|
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Unexpected event type or state key");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
client.getRoom = (getRoomId) => {
|
||||||
|
expect(getRoomId).toEqual(roomId);
|
||||||
|
return mockRoom;
|
||||||
|
};
|
||||||
|
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||||
|
expect(tree).toBeDefined();
|
||||||
|
expect(tree.roomId).toEqual(roomId);
|
||||||
|
expect(tree.room).toBe(mockRoom);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not get (unstable) file trees with invalid create contents", async () => {
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const mockRoom = {
|
||||||
|
currentState: {
|
||||||
|
getStateEvents: (eventType, stateKey) => {
|
||||||
|
if (eventType === EventType.RoomCreate) {
|
||||||
|
expect(stateKey).toEqual("");
|
||||||
|
return new MatrixEvent({
|
||||||
|
content: {
|
||||||
|
[RoomCreateTypeField]: "org.example.not_space",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
|
||||||
|
// We use `unstable` to ensure that the code is actually using the right identifier
|
||||||
|
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||||
|
return new MatrixEvent({
|
||||||
|
content: {
|
||||||
|
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Unexpected event type or state key");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
client.getRoom = (getRoomId) => {
|
||||||
|
expect(getRoomId).toEqual(roomId);
|
||||||
|
return mockRoom;
|
||||||
|
};
|
||||||
|
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||||
|
expect(tree).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
|
||||||
|
const roomId = "!room:example.org";
|
||||||
|
const mockRoom = {
|
||||||
|
currentState: {
|
||||||
|
getStateEvents: (eventType, stateKey) => {
|
||||||
|
if (eventType === EventType.RoomCreate) {
|
||||||
|
expect(stateKey).toEqual("");
|
||||||
|
return new MatrixEvent({
|
||||||
|
content: {
|
||||||
|
[RoomCreateTypeField]: RoomType.Space,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
|
||||||
|
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||||
|
return new MatrixEvent({
|
||||||
|
content: {
|
||||||
|
[UNSTABLE_MSC3088_ENABLED.unstable]: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Unexpected event type or state key");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
client.getRoom = (getRoomId) => {
|
||||||
|
expect(getRoomId).toEqual(roomId);
|
||||||
|
return mockRoom;
|
||||||
|
};
|
||||||
|
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||||
|
expect(tree).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
it("should not POST /filter if a matching filter already exists", async function() {
|
it("should not POST /filter if a matching filter already exists", async function() {
|
||||||
httpLookups = [];
|
httpLookups = [];
|
||||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||||
|
181
spec/unit/models/MSC3089TreeSpace.spec.ts
Normal file
181
spec/unit/models/MSC3089TreeSpace.spec.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixClient } from "../../../src";
|
||||||
|
import { Room } from "../../../src/models/room";
|
||||||
|
import { MatrixEvent } from "../../../src/models/event";
|
||||||
|
import { EventType } from "../../../src/@types/event";
|
||||||
|
import {
|
||||||
|
DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||||
|
MSC3089TreeSpace,
|
||||||
|
TreePermissions
|
||||||
|
} from "../../../src/models/MSC3089TreeSpace";
|
||||||
|
|
||||||
|
describe("MSC3089TreeSpace", () => {
|
||||||
|
let client: MatrixClient;
|
||||||
|
let room: Room;
|
||||||
|
let tree: MSC3089TreeSpace;
|
||||||
|
const roomId = "!tree:localhost";
|
||||||
|
const targetUser = "@target:example.org";
|
||||||
|
|
||||||
|
let powerLevels;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// TODO: Use utility functions to create test rooms and clients
|
||||||
|
client = <MatrixClient>{
|
||||||
|
getRoom: (roomId: string) => {
|
||||||
|
if (roomId === roomId) {
|
||||||
|
return room;
|
||||||
|
} else {
|
||||||
|
throw new Error("Unexpected fetch for unknown room");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
room = <Room>{
|
||||||
|
currentState: {
|
||||||
|
getStateEvents: (evType: EventType, stateKey: string) => {
|
||||||
|
if (evType === EventType.RoomPowerLevels && stateKey === "") {
|
||||||
|
return powerLevels;
|
||||||
|
} else {
|
||||||
|
throw new Error("Accessed unexpected state event type or key");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
tree = new MSC3089TreeSpace(client, roomId);
|
||||||
|
makePowerLevels(DEFAULT_TREE_POWER_LEVELS_TEMPLATE);
|
||||||
|
});
|
||||||
|
|
||||||
|
function makePowerLevels(content: any) {
|
||||||
|
powerLevels = new MatrixEvent({
|
||||||
|
type: EventType.RoomPowerLevels,
|
||||||
|
state_key: "",
|
||||||
|
sender: "@creator:localhost",
|
||||||
|
event_id: "$powerlevels",
|
||||||
|
room_id: roomId,
|
||||||
|
content: content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should populate the room reference', () => {
|
||||||
|
expect(tree.room).toBe(room);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should proxy the ID member to room ID', () => {
|
||||||
|
expect(tree.id).toEqual(tree.roomId);
|
||||||
|
expect(tree.id).toEqual(roomId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support setting the name of the space', async () => {
|
||||||
|
const newName = "NEW NAME";
|
||||||
|
const fn = jest.fn().mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||||
|
expect(stateRoomId).toEqual(roomId);
|
||||||
|
expect(eventType).toEqual(EventType.RoomName);
|
||||||
|
expect(stateKey).toEqual("");
|
||||||
|
expect(content).toMatchObject({name: newName});
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
client.sendStateEvent = fn;
|
||||||
|
await tree.setName(newName);
|
||||||
|
expect(fn.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support inviting users to the space', async () => {
|
||||||
|
const target = targetUser;
|
||||||
|
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||||
|
expect(inviteRoomId).toEqual(roomId);
|
||||||
|
expect(userId).toEqual(target);
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
client.invite = fn;
|
||||||
|
await tree.invite(target);
|
||||||
|
expect(fn.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
|
||||||
|
makePowerLevels(pls);
|
||||||
|
const fn = jest.fn().mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||||
|
expect(stateRoomId).toEqual(roomId);
|
||||||
|
expect(eventType).toEqual(EventType.RoomPowerLevels);
|
||||||
|
expect(stateKey).toEqual("");
|
||||||
|
expect(content).toMatchObject({
|
||||||
|
...pls,
|
||||||
|
users: {
|
||||||
|
[targetUser]: expectedPl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
client.sendStateEvent = fn;
|
||||||
|
await tree.setPermissions(targetUser, role);
|
||||||
|
expect(fn.mock.calls.length).toBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should support setting Viewer permissions', () => {
|
||||||
|
return evaluatePowerLevels({
|
||||||
|
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||||
|
users_default: 1024,
|
||||||
|
}, TreePermissions.Viewer, 1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support setting Editor permissions', () => {
|
||||||
|
return evaluatePowerLevels({
|
||||||
|
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||||
|
events_default: 1024,
|
||||||
|
}, TreePermissions.Editor, 1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support setting Owner permissions', () => {
|
||||||
|
return evaluatePowerLevels({
|
||||||
|
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||||
|
events: {
|
||||||
|
[EventType.RoomPowerLevels]: 1024,
|
||||||
|
},
|
||||||
|
}, TreePermissions.Owner, 1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support demoting permissions', () => {
|
||||||
|
return evaluatePowerLevels({
|
||||||
|
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||||
|
users_default: 1024,
|
||||||
|
users: {
|
||||||
|
[targetUser]: 2222,
|
||||||
|
}
|
||||||
|
}, TreePermissions.Viewer, 1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support promoting permissions', () => {
|
||||||
|
return evaluatePowerLevels({
|
||||||
|
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||||
|
events_default: 1024,
|
||||||
|
users: {
|
||||||
|
[targetUser]: 5,
|
||||||
|
}
|
||||||
|
}, TreePermissions.Editor, 1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support defaults: Viewer', () => {
|
||||||
|
return evaluatePowerLevels({}, TreePermissions.Viewer, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support defaults: Editor', () => {
|
||||||
|
return evaluatePowerLevels({}, TreePermissions.Editor, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support defaults: Owner', () => {
|
||||||
|
return evaluatePowerLevels({}, TreePermissions.Owner, 100);
|
||||||
|
});
|
||||||
|
});
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { UnstableValue } from "../NamespacedValue";
|
||||||
|
|
||||||
export enum EventType {
|
export enum EventType {
|
||||||
// Room state events
|
// Room state events
|
||||||
RoomCanonicalAlias = "m.room.canonical_alias",
|
RoomCanonicalAlias = "m.room.canonical_alias",
|
||||||
@ -100,3 +102,31 @@ export const RoomCreateTypeField = "type";
|
|||||||
export enum RoomType {
|
export enum RoomType {
|
||||||
Space = "m.space",
|
Space = "m.space",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifier for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
|
||||||
|
* room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
|
||||||
|
* including its eventual removal.
|
||||||
|
*/
|
||||||
|
export const UNSTABLE_MSC3088_PURPOSE = new UnstableValue("m.room.purpose", "org.matrix.msc3088.purpose");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enabled flag for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
|
||||||
|
* room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
|
||||||
|
* including its eventual removal.
|
||||||
|
*/
|
||||||
|
export const UNSTABLE_MSC3088_ENABLED = new UnstableValue("m.enabled", "org.matrix.msc3088.enabled");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtype for an [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room.
|
||||||
|
* Note that this reference is UNSTABLE and subject to breaking changes, including its
|
||||||
|
* eventual removal.
|
||||||
|
*/
|
||||||
|
export const UNSTABLE_MSC3089_TREE_SUBTYPE = new UnstableValue("m.data_tree", "org.matrix.msc3089.data_tree");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leaf type for an event in a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room.
|
||||||
|
* Note that this reference is UNSTABLE and subject to breaking changes, including its
|
||||||
|
* eventual removal.
|
||||||
|
*/
|
||||||
|
export const UNSTABLE_MSC3089_LEAF = new UnstableValue("m.leaf", "org.matrix.msc3089.leaf");
|
||||||
|
@ -74,6 +74,9 @@ export interface ICreateRoomOpts {
|
|||||||
name?: string;
|
name?: string;
|
||||||
topic?: string;
|
topic?: string;
|
||||||
preset?: string;
|
preset?: string;
|
||||||
|
power_level_content_override?: any;
|
||||||
|
creation_content?: any;
|
||||||
|
initial_state?: {type: string, state_key: string, content: any}[];
|
||||||
// TODO: Types (next line)
|
// TODO: Types (next line)
|
||||||
invite_3pid?: any[]; // eslint-disable-line camelcase
|
invite_3pid?: any[]; // eslint-disable-line camelcase
|
||||||
}
|
}
|
||||||
|
95
src/NamespacedValue.ts
Normal file
95
src/NamespacedValue.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a simple Matrix namespaced value. This will assume that if a stable prefix
|
||||||
|
* is provided that the stable prefix should be used when representing the identifier.
|
||||||
|
*/
|
||||||
|
export class NamespacedValue<S extends string, U extends string> {
|
||||||
|
public constructor(public readonly stable: S, public readonly unstable?: U) {
|
||||||
|
if (!this.unstable && !this.stable) {
|
||||||
|
throw new Error("One of stable or unstable values must be supplied");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get tsType(): U | S {
|
||||||
|
return null; // irrelevant return
|
||||||
|
}
|
||||||
|
|
||||||
|
public get name(): U | S {
|
||||||
|
if (this.stable) {
|
||||||
|
return this.stable;
|
||||||
|
}
|
||||||
|
return this.unstable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get altName(): U | S | null {
|
||||||
|
if (!this.stable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.unstable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public matches(val: string): boolean {
|
||||||
|
return this.name === val || this.altName === val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
|
||||||
|
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
|
||||||
|
public findIn<T>(obj: any): T {
|
||||||
|
let val: T;
|
||||||
|
if (this.name) {
|
||||||
|
val = obj?.[this.name];
|
||||||
|
}
|
||||||
|
if (!val && this.altName) {
|
||||||
|
val = obj?.[this.altName];
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public includedIn(arr: any[]): boolean {
|
||||||
|
let included = false;
|
||||||
|
if (this.name) {
|
||||||
|
included = arr.includes(this.name);
|
||||||
|
}
|
||||||
|
if (!included && this.altName) {
|
||||||
|
included = arr.includes(this.altName);
|
||||||
|
}
|
||||||
|
return included;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a namespaced value which prioritizes the unstable value over the stable
|
||||||
|
* value.
|
||||||
|
*/
|
||||||
|
export class UnstableValue<S extends string, U extends string> extends NamespacedValue<S, U> {
|
||||||
|
// Note: Constructor difference is that `unstable` is *required*.
|
||||||
|
public constructor(stable: S, unstable: U) {
|
||||||
|
super(stable, unstable);
|
||||||
|
if (!this.unstable) {
|
||||||
|
throw new Error("Unstable value must be supplied");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get name(): U {
|
||||||
|
return this.unstable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get altName(): S {
|
||||||
|
return this.stable;
|
||||||
|
}
|
||||||
|
}
|
@ -99,7 +99,13 @@ import {
|
|||||||
ISendEventResponse,
|
ISendEventResponse,
|
||||||
IUploadOpts,
|
IUploadOpts,
|
||||||
} from "./@types/requests";
|
} from "./@types/requests";
|
||||||
import { EventType } from "./@types/event";
|
import {
|
||||||
|
EventType,
|
||||||
|
RoomCreateTypeField,
|
||||||
|
RoomType,
|
||||||
|
UNSTABLE_MSC3088_ENABLED,
|
||||||
|
UNSTABLE_MSC3088_PURPOSE, UNSTABLE_MSC3089_TREE_SUBTYPE
|
||||||
|
} from "./@types/event";
|
||||||
import { IImageInfo } from "./@types/partials";
|
import { IImageInfo } from "./@types/partials";
|
||||||
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper";
|
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper";
|
||||||
import url from "url";
|
import url from "url";
|
||||||
@ -107,6 +113,7 @@ import { randomString } from "./randomstring";
|
|||||||
import { ReadStream } from "fs";
|
import { ReadStream } from "fs";
|
||||||
import { WebStorageSessionStore } from "./store/session/webstorage";
|
import { WebStorageSessionStore } from "./store/session/webstorage";
|
||||||
import { BackupManager } from "./crypto/backup";
|
import { BackupManager } from "./crypto/backup";
|
||||||
|
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE, MSC3089TreeSpace } from "./models/MSC3089TreeSpace";
|
||||||
|
|
||||||
export type Store = StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
|
export type Store = StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
|
||||||
export type SessionStore = WebStorageSessionStore;
|
export type SessionStore = WebStorageSessionStore;
|
||||||
@ -7709,6 +7716,74 @@ export class MatrixClient extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new file tree space with the given name. The client will pick
|
||||||
|
* defaults for how it expects to be able to support the remaining API offered
|
||||||
|
* by the returned class.
|
||||||
|
*
|
||||||
|
* Note that this is UNSTABLE and may have breaking changes without notice.
|
||||||
|
* @param {string} name The name of the tree space.
|
||||||
|
* @returns {Promise<MSC3089TreeSpace>} Resolves to the created space.
|
||||||
|
*/
|
||||||
|
public async unstableCreateFileTree(name: string): Promise<MSC3089TreeSpace> {
|
||||||
|
const { room_id: roomId } = await this.createRoom({
|
||||||
|
name: name,
|
||||||
|
preset: "private_chat",
|
||||||
|
power_level_content_override: {
|
||||||
|
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||||
|
users: {
|
||||||
|
[this.getUserId()]: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
creation_content: {
|
||||||
|
[RoomCreateTypeField]: RoomType.Space,
|
||||||
|
},
|
||||||
|
initial_state: [
|
||||||
|
{
|
||||||
|
type: UNSTABLE_MSC3088_PURPOSE.name,
|
||||||
|
state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.name,
|
||||||
|
content: {
|
||||||
|
[UNSTABLE_MSC3088_ENABLED.name]: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: EventType.RoomEncryption,
|
||||||
|
state_key: "",
|
||||||
|
content: {
|
||||||
|
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return new MSC3089TreeSpace(this, roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a reference to a tree space, if the room ID given is a tree space. If the room
|
||||||
|
* does not appear to be a tree space then null is returned.
|
||||||
|
*
|
||||||
|
* Note that this is UNSTABLE and may have breaking changes without notice.
|
||||||
|
* @param {string} roomId The room ID to get a tree space reference for.
|
||||||
|
* @returns {MSC3089TreeSpace} The tree space, or null if not a tree space.
|
||||||
|
*/
|
||||||
|
public unstableGetFileTreeSpace(roomId: string): MSC3089TreeSpace {
|
||||||
|
const room = this.getRoom(roomId);
|
||||||
|
if (!room) return null;
|
||||||
|
|
||||||
|
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||||
|
const purposeEvent = room.currentState.getStateEvents(
|
||||||
|
UNSTABLE_MSC3088_PURPOSE.name,
|
||||||
|
UNSTABLE_MSC3089_TREE_SUBTYPE.name);
|
||||||
|
|
||||||
|
if (!createEvent || Array.isArray(createEvent)) throw new Error("Expected single room create event");
|
||||||
|
if (!purposeEvent || Array.isArray(purposeEvent)) return null;
|
||||||
|
|
||||||
|
if (!purposeEvent.getContent()?.[UNSTABLE_MSC3088_ENABLED.name]) return null;
|
||||||
|
if (createEvent.getContent()?.[RoomCreateTypeField] !== RoomType.Space) return null;
|
||||||
|
|
||||||
|
return new MSC3089TreeSpace(this, roomId);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Remove this warning, alongside the functions
|
// TODO: Remove this warning, alongside the functions
|
||||||
// See https://github.com/vector-im/element-web/issues/17532
|
// See https://github.com/vector-im/element-web/issues/17532
|
||||||
// ======================================================
|
// ======================================================
|
||||||
|
138
src/models/MSC3089TreeSpace.ts
Normal file
138
src/models/MSC3089TreeSpace.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixClient } from "../client";
|
||||||
|
import { EventType } from "../@types/event";
|
||||||
|
import { Room } from "./room";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The recommended defaults for a tree space's power levels. Note that this
|
||||||
|
* is UNSTABLE and subject to breaking changes without notice.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_TREE_POWER_LEVELS_TEMPLATE = {
|
||||||
|
// Owner
|
||||||
|
invite: 100,
|
||||||
|
kick: 100,
|
||||||
|
ban: 100,
|
||||||
|
|
||||||
|
// Editor
|
||||||
|
redact: 50,
|
||||||
|
state_default: 50,
|
||||||
|
events_default: 50,
|
||||||
|
|
||||||
|
// Viewer
|
||||||
|
users_default: 0,
|
||||||
|
|
||||||
|
// Mixed
|
||||||
|
events: {
|
||||||
|
[EventType.RoomPowerLevels]: 100,
|
||||||
|
[EventType.RoomHistoryVisibility]: 100,
|
||||||
|
[EventType.RoomTombstone]: 100,
|
||||||
|
[EventType.RoomEncryption]: 100,
|
||||||
|
[EventType.RoomName]: 50,
|
||||||
|
[EventType.RoomMessage]: 50,
|
||||||
|
[EventType.RoomMessageEncrypted]: 50,
|
||||||
|
[EventType.Sticker]: 50,
|
||||||
|
},
|
||||||
|
users: {}, // defined by calling code
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ease-of-use representation for power levels represented as simple roles.
|
||||||
|
* Note that this is UNSTABLE and subject to breaking changes without notice.
|
||||||
|
*/
|
||||||
|
export enum TreePermissions {
|
||||||
|
Viewer = "viewer", // Default
|
||||||
|
Editor = "editor", // "Moderator" or ~PL50
|
||||||
|
Owner = "owner", // "Admin" or PL100
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089)
|
||||||
|
* file tree Space. Note that this is UNSTABLE and subject to breaking changes
|
||||||
|
* without notice.
|
||||||
|
*/
|
||||||
|
export class MSC3089TreeSpace {
|
||||||
|
public readonly room: Room;
|
||||||
|
|
||||||
|
public constructor(private client: MatrixClient, public readonly roomId: string) {
|
||||||
|
this.room = this.client.getRoom(this.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syntactic sugar for room ID of the Space.
|
||||||
|
*/
|
||||||
|
public get id(): string {
|
||||||
|
return this.roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the name of the tree space.
|
||||||
|
* @param {string} name The new name for the space.
|
||||||
|
* @returns {Promise<void>} Resolves when complete.
|
||||||
|
*/
|
||||||
|
public setName(name: string): Promise<void> {
|
||||||
|
return this.client.sendStateEvent(this.roomId, EventType.RoomName, {name}, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invites a user to the tree space. They will be given the default Viewer
|
||||||
|
* permission level unless specified elsewhere.
|
||||||
|
* @param {string} userId The user ID to invite.
|
||||||
|
* @returns {Promise<void>} Resolves when complete.
|
||||||
|
*/
|
||||||
|
public invite(userId: string): Promise<void> {
|
||||||
|
// TODO: [@@TR] Reliable invites
|
||||||
|
// TODO: [@@TR] Share keys
|
||||||
|
return this.client.invite(this.roomId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the permissions of a user to the given role. Note that if setting a user
|
||||||
|
* to Owner then they will NOT be able to be demoted. If the user does not have
|
||||||
|
* permission to change the power level of the target, an error will be thrown.
|
||||||
|
* @param {string} userId The user ID to change the role of.
|
||||||
|
* @param {TreePermissions} role The role to assign.
|
||||||
|
* @returns {Promise<void>} Resolves when complete.
|
||||||
|
*/
|
||||||
|
public async setPermissions(userId: string, role: TreePermissions): Promise<void> {
|
||||||
|
const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||||
|
if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
|
||||||
|
|
||||||
|
const pls = currentPls.getContent() || {};
|
||||||
|
const viewLevel = pls['users_default'] || 0;
|
||||||
|
const editLevel = pls['events_default'] || 50;
|
||||||
|
const adminLevel = pls['events']?.[EventType.RoomPowerLevels] || 100;
|
||||||
|
|
||||||
|
const users = pls['users'] || {};
|
||||||
|
switch (role) {
|
||||||
|
case TreePermissions.Viewer:
|
||||||
|
users[userId] = viewLevel;
|
||||||
|
break;
|
||||||
|
case TreePermissions.Editor:
|
||||||
|
users[userId] = editLevel;
|
||||||
|
break;
|
||||||
|
case TreePermissions.Owner:
|
||||||
|
users[userId] = adminLevel;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid role: " + role);
|
||||||
|
}
|
||||||
|
pls['users'] = users;
|
||||||
|
|
||||||
|
return this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user