You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-12-04 05:02:41 +03:00
Support for creator power level (#4937)
* Support for creator power level Adds support for infinite power level specified by [MSC4289](https://github.com/matrix-org/matrix-spec-proposals/pull/4289). * Update unit test * Hardcode versions as room versions strings aren't ordered * Add test for v12 rooms * Use more compact syntax Co-authored-by: R Midhun Suresh <hi@midhun.dev> * Fix doc Co-authored-by: R Midhun Suresh <hi@midhun.dev> * Fix additionalCreators from PR edit * Split out hydra room version check * Move power level logic into room state Which already has knowledge of the room create event * Add docs * Fix unused bits * Fix docs * Fix lying docstring * Reverse logic for hydra semantics Assume unknown room versions do use hydra * Use backticks Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Switch back to hardcoding just the two hydra versions --------- Co-authored-by: R Midhun Suresh <hi@midhun.dev> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
@@ -20,8 +20,8 @@ import * as utils from "../test-utils/test-utils";
|
|||||||
import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
|
import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
|
||||||
import {
|
import {
|
||||||
createClient,
|
createClient,
|
||||||
EventType,
|
|
||||||
type MatrixClient,
|
type MatrixClient,
|
||||||
|
MatrixEvent,
|
||||||
type RoomState,
|
type RoomState,
|
||||||
UNSTABLE_MSC2666_MUTUAL_ROOMS,
|
UNSTABLE_MSC2666_MUTUAL_ROOMS,
|
||||||
UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS,
|
UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS,
|
||||||
@@ -101,158 +101,32 @@ describe("RoomMember", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setPowerLevelEvent", function () {
|
describe("setPowerLevel", function () {
|
||||||
it("should set 'powerLevel' and 'powerLevelNorm'.", function () {
|
it("should set 'powerLevel'.", function () {
|
||||||
const event = utils.mkEvent({
|
member.setPowerLevel(0, new MatrixEvent());
|
||||||
type: "m.room.power_levels",
|
|
||||||
room: roomId,
|
|
||||||
user: userA,
|
|
||||||
content: {
|
|
||||||
users_default: 20,
|
|
||||||
users: {
|
|
||||||
"@bertha:bar": 200,
|
|
||||||
"@invalid:user": 10, // shouldn't barf on this.
|
|
||||||
},
|
|
||||||
},
|
|
||||||
event: true,
|
|
||||||
});
|
|
||||||
member.setPowerLevelEvent(event);
|
|
||||||
expect(member.powerLevel).toEqual(20);
|
|
||||||
expect(member.powerLevelNorm).toEqual(10);
|
|
||||||
|
|
||||||
const memberB = new RoomMember(roomId, userB);
|
|
||||||
memberB.setPowerLevelEvent(event);
|
|
||||||
expect(memberB.powerLevel).toEqual(200);
|
|
||||||
expect(memberB.powerLevelNorm).toEqual(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should emit 'RoomMember.powerLevel' if the power level changes.", function () {
|
|
||||||
const event = utils.mkEvent({
|
|
||||||
type: "m.room.power_levels",
|
|
||||||
room: roomId,
|
|
||||||
user: userA,
|
|
||||||
content: {
|
|
||||||
users_default: 20,
|
|
||||||
users: {
|
|
||||||
"@bertha:bar": 200,
|
|
||||||
"@invalid:user": 10, // shouldn't barf on this.
|
|
||||||
},
|
|
||||||
},
|
|
||||||
event: true,
|
|
||||||
});
|
|
||||||
let emitCount = 0;
|
|
||||||
|
|
||||||
member.on(RoomMemberEvent.PowerLevel, function (emitEvent, emitMember) {
|
|
||||||
emitCount += 1;
|
|
||||||
expect(emitMember).toEqual(member);
|
|
||||||
expect(emitEvent).toEqual(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
member.setPowerLevelEvent(event);
|
|
||||||
expect(emitCount).toEqual(1);
|
|
||||||
member.setPowerLevelEvent(event); // no-op
|
|
||||||
expect(emitCount).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should honour power levels of zero.", function () {
|
|
||||||
const event = utils.mkEvent({
|
|
||||||
type: "m.room.power_levels",
|
|
||||||
room: roomId,
|
|
||||||
user: userA,
|
|
||||||
content: {
|
|
||||||
users_default: 20,
|
|
||||||
users: {
|
|
||||||
"@alice:bar": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
event: true,
|
|
||||||
});
|
|
||||||
let emitCount = 0;
|
|
||||||
|
|
||||||
// set the power level to something other than zero or we
|
|
||||||
// won't get an event
|
|
||||||
member.powerLevel = 1;
|
|
||||||
member.on(RoomMemberEvent.PowerLevel, function (emitEvent, emitMember) {
|
|
||||||
emitCount += 1;
|
|
||||||
expect(emitMember.userId).toEqual("@alice:bar");
|
|
||||||
expect(emitMember.powerLevel).toEqual(0);
|
|
||||||
expect(emitEvent).toEqual(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
member.setPowerLevelEvent(event);
|
|
||||||
expect(member.powerLevel).toEqual(0);
|
expect(member.powerLevel).toEqual(0);
|
||||||
expect(emitCount).toEqual(1);
|
member.setPowerLevel(200, new MatrixEvent());
|
||||||
|
expect(member.powerLevel).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not honor string power levels.", function () {
|
it("should emit when power level set", function () {
|
||||||
const event = utils.mkEvent({
|
const onEmit = jest.fn();
|
||||||
type: "m.room.power_levels",
|
member.on(RoomMemberEvent.PowerLevel, onEmit);
|
||||||
room: roomId,
|
|
||||||
user: userA,
|
|
||||||
content: {
|
|
||||||
users_default: 20,
|
|
||||||
users: {
|
|
||||||
"@alice:bar": "5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
event: true,
|
|
||||||
});
|
|
||||||
let emitCount = 0;
|
|
||||||
|
|
||||||
member.on(RoomMemberEvent.PowerLevel, function (emitEvent, emitMember) {
|
const aMatrixEvent = new MatrixEvent();
|
||||||
emitCount += 1;
|
member.setPowerLevel(10, aMatrixEvent);
|
||||||
expect(emitMember.userId).toEqual("@alice:bar");
|
|
||||||
expect(emitMember.powerLevel).toEqual(20);
|
|
||||||
expect(emitEvent).toEqual(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
member.setPowerLevelEvent(event);
|
expect(onEmit).toHaveBeenCalledWith(aMatrixEvent, member);
|
||||||
expect(member.powerLevel).toEqual(20);
|
|
||||||
expect(emitCount).toEqual(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should no-op if given a non-state or unrelated event", () => {
|
it("should not emit if new power level is the same", function () {
|
||||||
const fn = jest.spyOn(member, "emit");
|
const onEmit = jest.fn();
|
||||||
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
|
member.on(RoomMemberEvent.PowerLevel, onEmit);
|
||||||
member.setPowerLevelEvent(
|
|
||||||
utils.mkEvent({
|
const aMatrixEvent = new MatrixEvent();
|
||||||
type: EventType.RoomPowerLevels,
|
member.setPowerLevel(0, aMatrixEvent);
|
||||||
room: roomId,
|
|
||||||
user: userA,
|
expect(onEmit).not.toHaveBeenCalled();
|
||||||
content: {
|
|
||||||
users_default: 20,
|
|
||||||
users: {
|
|
||||||
"@alice:bar": "5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
skey: "invalid",
|
|
||||||
event: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const nonStateEv = utils.mkEvent({
|
|
||||||
type: EventType.RoomPowerLevels,
|
|
||||||
room: roomId,
|
|
||||||
user: userA,
|
|
||||||
content: {
|
|
||||||
users_default: 20,
|
|
||||||
users: {
|
|
||||||
"@alice:bar": "5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
event: true,
|
|
||||||
});
|
|
||||||
delete nonStateEv.event.state_key;
|
|
||||||
member.setPowerLevelEvent(nonStateEv);
|
|
||||||
member.setPowerLevelEvent(
|
|
||||||
utils.mkEvent({
|
|
||||||
type: EventType.Sticker,
|
|
||||||
room: roomId,
|
|
||||||
user: userA,
|
|
||||||
content: {},
|
|
||||||
event: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import * as utils from "../test-utils/test-utils";
|
|||||||
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
||||||
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
||||||
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
||||||
|
import { RoomMemberEvent } from "../../src/models/room-member";
|
||||||
import { type Beacon, BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
|
import { type Beacon, BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
|
||||||
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
|
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
|
||||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||||
@@ -259,7 +260,7 @@ describe("RoomState", function () {
|
|||||||
expect(emitCount).toEqual(2);
|
expect(emitCount).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call setPowerLevelEvent on each RoomMember for m.room.power_levels", function () {
|
it("should call setPowerLevel on each RoomMember for m.room.power_levels", function () {
|
||||||
const powerLevelEvent = utils.mkEvent({
|
const powerLevelEvent = utils.mkEvent({
|
||||||
type: "m.room.power_levels",
|
type: "m.room.power_levels",
|
||||||
room: roomId,
|
room: roomId,
|
||||||
@@ -273,12 +274,12 @@ describe("RoomState", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// spy on the room members
|
// spy on the room members
|
||||||
jest.spyOn(state.members[userA], "setPowerLevelEvent");
|
jest.spyOn(state.members[userA], "setPowerLevel");
|
||||||
jest.spyOn(state.members[userB], "setPowerLevelEvent");
|
jest.spyOn(state.members[userB], "setPowerLevel");
|
||||||
state.setStateEvents([powerLevelEvent]);
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
|
||||||
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(powerLevelEvent);
|
expect(state.members[userA].setPowerLevel).toHaveBeenCalledWith(10, powerLevelEvent);
|
||||||
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(powerLevelEvent);
|
expect(state.members[userB].setPowerLevel).toHaveBeenCalledWith(10, powerLevelEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call setPowerLevelEvent on a new RoomMember if power levels exist", function () {
|
it("should call setPowerLevelEvent on a new RoomMember if power levels exist", function () {
|
||||||
@@ -310,6 +311,156 @@ describe("RoomState", function () {
|
|||||||
expect(state.members[userC].powerLevel).toEqual(10);
|
expect(state.members[userC].powerLevel).toEqual(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should calculate power level correctly", function () {
|
||||||
|
const powerLevelEvent = utils.mkEvent({
|
||||||
|
type: "m.room.power_levels",
|
||||||
|
room: roomId,
|
||||||
|
user: userA,
|
||||||
|
content: {
|
||||||
|
users_default: 20,
|
||||||
|
users: {
|
||||||
|
[userB]: 200,
|
||||||
|
"@invalid:user": 10, // shouldn't barf on this.
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
|
||||||
|
expect(state.getMember(userA)?.powerLevel).toEqual(20);
|
||||||
|
expect(state.getMember(userB)?.powerLevel).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set 'powerLevel' with a v12 room.", function () {
|
||||||
|
const createEventV12 = utils.mkEvent({
|
||||||
|
type: "m.room.create",
|
||||||
|
room: roomId,
|
||||||
|
sender: userA,
|
||||||
|
content: { room_version: "12" },
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
const powerLevelEvent = utils.mkEvent({
|
||||||
|
type: "m.room.power_levels",
|
||||||
|
room: roomId,
|
||||||
|
user: userA,
|
||||||
|
content: {
|
||||||
|
users_default: 20,
|
||||||
|
users: {
|
||||||
|
[userB]: 200,
|
||||||
|
"@invalid:user": 10, // shouldn't barf on this.
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
state.setStateEvents([createEventV12, powerLevelEvent]);
|
||||||
|
expect(state.getMember(userA)?.powerLevel).toEqual(Infinity);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should honour power levels of zero.", function () {
|
||||||
|
const powerLevelEvent = utils.mkEvent({
|
||||||
|
type: "m.room.power_levels",
|
||||||
|
room: roomId,
|
||||||
|
user: userA,
|
||||||
|
content: {
|
||||||
|
users_default: 20,
|
||||||
|
users: {
|
||||||
|
"@alice:bar": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
let emitCount = 0;
|
||||||
|
|
||||||
|
const memberA = state.getMember(userA)!;
|
||||||
|
// set the power level to something other than zero or we
|
||||||
|
// won't get an event
|
||||||
|
memberA.powerLevel = 1;
|
||||||
|
memberA.on(RoomMemberEvent.PowerLevel, function (emitEvent, emitMember) {
|
||||||
|
emitCount += 1;
|
||||||
|
expect(emitMember.userId).toEqual("@alice:bar");
|
||||||
|
expect(emitMember.powerLevel).toEqual(0);
|
||||||
|
expect(emitEvent).toEqual(powerLevelEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
expect(memberA.powerLevel).toEqual(0);
|
||||||
|
expect(emitCount).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not honor string power levels.", function () {
|
||||||
|
const powerLevelEvent = utils.mkEvent({
|
||||||
|
type: "m.room.power_levels",
|
||||||
|
room: roomId,
|
||||||
|
user: userA,
|
||||||
|
content: {
|
||||||
|
users_default: 20,
|
||||||
|
users: {
|
||||||
|
"@alice:bar": "5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
let emitCount = 0;
|
||||||
|
|
||||||
|
const memberA = state.getMember(userA)!;
|
||||||
|
memberA.on(RoomMemberEvent.PowerLevel, function (emitEvent, emitMember) {
|
||||||
|
emitCount += 1;
|
||||||
|
expect(emitMember.userId).toEqual("@alice:bar");
|
||||||
|
expect(emitMember.powerLevel).toEqual(20);
|
||||||
|
expect(emitEvent).toEqual(powerLevelEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
expect(memberA.powerLevel).toEqual(20);
|
||||||
|
expect(emitCount).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should no-op if given a non-state or unrelated event", () => {
|
||||||
|
const memberA = state.getMember(userA)!;
|
||||||
|
const fn = jest.spyOn(memberA, "emit");
|
||||||
|
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
|
||||||
|
|
||||||
|
const powerLevelEvent = utils.mkEvent({
|
||||||
|
type: EventType.RoomPowerLevels,
|
||||||
|
room: roomId,
|
||||||
|
user: userA,
|
||||||
|
content: {
|
||||||
|
users_default: 20,
|
||||||
|
users: {
|
||||||
|
"@alice:bar": "5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
skey: "invalid",
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
const nonStateEv = utils.mkEvent({
|
||||||
|
type: EventType.RoomPowerLevels,
|
||||||
|
room: roomId,
|
||||||
|
user: userA,
|
||||||
|
content: {
|
||||||
|
users_default: 20,
|
||||||
|
users: {
|
||||||
|
"@alice:bar": "5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
delete nonStateEv.event.state_key;
|
||||||
|
state.setStateEvents([nonStateEv]);
|
||||||
|
state.setStateEvents([
|
||||||
|
utils.mkEvent({
|
||||||
|
type: EventType.Sticker,
|
||||||
|
room: roomId,
|
||||||
|
user: userA,
|
||||||
|
content: {},
|
||||||
|
event: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
|
||||||
|
});
|
||||||
|
|
||||||
it("should call setMembershipEvent on the right RoomMember", function () {
|
it("should call setMembershipEvent on the right RoomMember", function () {
|
||||||
const memberEvent = utils.mkMembership({
|
const memberEvent = utils.mkMembership({
|
||||||
user: userB,
|
user: userB,
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ export type RoomMemberEventHandlerMap = {
|
|||||||
* ```
|
* ```
|
||||||
* matrixClient.on("RoomMember.powerLevel", function(event, member){
|
* matrixClient.on("RoomMember.powerLevel", function(event, member){
|
||||||
* var newPowerLevel = member.powerLevel;
|
* var newPowerLevel = member.powerLevel;
|
||||||
* var newNormPowerLevel = member.powerLevelNorm;
|
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -109,10 +108,6 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
|||||||
* The power level for this room member.
|
* The power level for this room member.
|
||||||
*/
|
*/
|
||||||
public powerLevel = 0;
|
public powerLevel = 0;
|
||||||
/**
|
|
||||||
* The normalised power level (0-100) for this room member.
|
|
||||||
*/
|
|
||||||
public powerLevelNorm = 0;
|
|
||||||
/**
|
/**
|
||||||
* The User object for this room member, if one exists.
|
* The User object for this room member, if one exists.
|
||||||
*/
|
*/
|
||||||
@@ -226,43 +221,18 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update this room member's power level event. May fire
|
* Update this room member's power level event. Will fire
|
||||||
* "RoomMember.powerLevel" if this event updates this member's power levels.
|
* "RoomMember.powerLevel" if the new power level is different
|
||||||
* @param powerLevelEvent - The `m.room.power_levels` event
|
* @param powerLevel - The power level of the room member.
|
||||||
*
|
*
|
||||||
* @remarks
|
* @remarks
|
||||||
* Fires {@link RoomMemberEvent.PowerLevel}
|
* Fires {@link RoomMemberEvent.PowerLevel}
|
||||||
*/
|
*/
|
||||||
public setPowerLevelEvent(powerLevelEvent: MatrixEvent): void {
|
public setPowerLevel(powerLevel: number, powerLevelEvent: MatrixEvent): void {
|
||||||
if (powerLevelEvent.getType() !== EventType.RoomPowerLevels || powerLevelEvent.getStateKey() !== "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const evContent = powerLevelEvent.getDirectionalContent();
|
|
||||||
|
|
||||||
let maxLevel = evContent.users_default || 0;
|
|
||||||
const users: { [userId: string]: number } = evContent.users || {};
|
|
||||||
Object.values(users).forEach((lvl: number) => {
|
|
||||||
maxLevel = Math.max(maxLevel, lvl);
|
|
||||||
});
|
|
||||||
const oldPowerLevel = this.powerLevel;
|
const oldPowerLevel = this.powerLevel;
|
||||||
const oldPowerLevelNorm = this.powerLevelNorm;
|
this.powerLevel = powerLevel;
|
||||||
|
|
||||||
if (users[this.userId] !== undefined && Number.isInteger(users[this.userId])) {
|
if (oldPowerLevel !== this.powerLevel) {
|
||||||
this.powerLevel = users[this.userId];
|
|
||||||
} else if (evContent.users_default !== undefined) {
|
|
||||||
this.powerLevel = evContent.users_default;
|
|
||||||
} else {
|
|
||||||
this.powerLevel = 0;
|
|
||||||
}
|
|
||||||
this.powerLevelNorm = 0;
|
|
||||||
if (maxLevel > 0) {
|
|
||||||
this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// emit for changes in powerLevelNorm as well (since the app will need to
|
|
||||||
// redraw everyone's level if the max has changed)
|
|
||||||
if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
|
|
||||||
this.updateModifiedTime();
|
this.updateModifiedTime();
|
||||||
this.emit(RoomMemberEvent.PowerLevel, powerLevelEvent, this);
|
this.emit(RoomMemberEvent.PowerLevel, powerLevelEvent, this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { TypedReEmitter } from "../ReEmitter.ts";
|
|||||||
import { M_BEACON, M_BEACON_INFO } from "../@types/beacon.ts";
|
import { M_BEACON, M_BEACON_INFO } from "../@types/beacon.ts";
|
||||||
import { KnownMembership } from "../@types/membership.ts";
|
import { KnownMembership } from "../@types/membership.ts";
|
||||||
import { type RoomJoinRulesEventContent } from "../@types/state_events.ts";
|
import { type RoomJoinRulesEventContent } from "../@types/state_events.ts";
|
||||||
|
import { shouldUseHydraForRoomVersion } from "../utils/roomVersion.ts";
|
||||||
|
|
||||||
export interface IMarkerFoundOptions {
|
export interface IMarkerFoundOptions {
|
||||||
/** Whether the timeline was empty before the marker event arrived in the
|
/** Whether the timeline was empty before the marker event arrived in the
|
||||||
@@ -173,6 +174,9 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
|
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
|
||||||
private _liveBeaconIds: BeaconIdentifier[] = [];
|
private _liveBeaconIds: BeaconIdentifier[] = [];
|
||||||
|
|
||||||
|
// We only wants to print warnings about bad room state once.
|
||||||
|
private getVersionWarning = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct room state.
|
* Construct room state.
|
||||||
*
|
*
|
||||||
@@ -209,6 +213,22 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
this.updateModifiedTime();
|
this.updateModifiedTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the version of the room
|
||||||
|
* @returns The version of the room
|
||||||
|
*/
|
||||||
|
public getRoomVersion(): string {
|
||||||
|
const createEvent = this.getStateEvents(EventType.RoomCreate, "");
|
||||||
|
if (!createEvent) {
|
||||||
|
if (!this.getVersionWarning) {
|
||||||
|
logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event");
|
||||||
|
this.getVersionWarning = true;
|
||||||
|
}
|
||||||
|
return "1";
|
||||||
|
}
|
||||||
|
return createEvent.getContent()["room_version"] ?? "1";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of joined members in this room
|
* Returns the number of joined members in this room
|
||||||
* This method caches the result.
|
* This method caches the result.
|
||||||
@@ -468,12 +488,20 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const members = Object.values(this.members);
|
const members = Object.values(this.members);
|
||||||
|
|
||||||
|
const createEvent = this.getStateEvents(EventType.RoomCreate, "");
|
||||||
|
const creators = getCreators(this.getRoomVersion(), createEvent);
|
||||||
|
|
||||||
members.forEach((member) => {
|
members.forEach((member) => {
|
||||||
// We only propagate `RoomState.members` event if the
|
// We only propagate `RoomState.members` event if the
|
||||||
// power levels has been changed
|
// power levels has been changed
|
||||||
// large room suffer from large re-rendering especially when not needed
|
// large room suffer from large re-rendering especially when not needed
|
||||||
const oldLastModified = member.getLastModifiedTime();
|
const oldLastModified = member.getLastModifiedTime();
|
||||||
member.setPowerLevelEvent(event);
|
|
||||||
|
if (createEvent) {
|
||||||
|
const pl = powerLevelForUserId(member.userId, event, creators);
|
||||||
|
member.setPowerLevel(pl, event);
|
||||||
|
}
|
||||||
if (oldLastModified !== member.getLastModifiedTime()) {
|
if (oldLastModified !== member.getLastModifiedTime()) {
|
||||||
this.emit(RoomStateEvent.Members, event, this, member);
|
this.emit(RoomStateEvent.Members, event, this, member);
|
||||||
}
|
}
|
||||||
@@ -625,9 +653,16 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
|
|
||||||
private updateMember(member: RoomMember): void {
|
private updateMember(member: RoomMember): void {
|
||||||
// this member may have a power level already, so set it.
|
// this member may have a power level already, so set it.
|
||||||
|
const createEvent = this.getStateEvents(EventType.RoomCreate, "");
|
||||||
const pwrLvlEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
|
const pwrLvlEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
|
||||||
if (pwrLvlEvent) {
|
if (pwrLvlEvent && createEvent) {
|
||||||
member.setPowerLevelEvent(pwrLvlEvent);
|
const powerLevel = powerLevelForUserId(
|
||||||
|
member.userId,
|
||||||
|
pwrLvlEvent,
|
||||||
|
getCreators(this.getRoomVersion(), createEvent),
|
||||||
|
);
|
||||||
|
|
||||||
|
member.setPowerLevel(powerLevel, pwrLvlEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// blow away the sentinel which is now outdated
|
// blow away the sentinel which is now outdated
|
||||||
@@ -1106,3 +1141,46 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the set of creator user IDs for a room: empty if the room is not a 'hydra' room, otherwise
|
||||||
|
* computed from the sender of the m.room.create event plus the additional_creators field.
|
||||||
|
* @param roomVersion The version of the room
|
||||||
|
* @param roomCreateEvent The m.room.create event for the room
|
||||||
|
* @returns A set of user IDs of the creators of the room.
|
||||||
|
*/
|
||||||
|
function getCreators(roomVersion: string, roomCreateEvent: MatrixEvent | null): Set<string> {
|
||||||
|
const creators = new Set<string>();
|
||||||
|
if (shouldUseHydraForRoomVersion(roomVersion) && roomCreateEvent) {
|
||||||
|
const roomCreateSender = roomCreateEvent.getSender();
|
||||||
|
if (roomCreateSender) creators.add(roomCreateSender);
|
||||||
|
const additionalCreators = roomCreateEvent.getDirectionalContent().additional_creators;
|
||||||
|
if (Array.isArray(additionalCreators)) additionalCreators.forEach((c) => creators.add(c));
|
||||||
|
}
|
||||||
|
return creators;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param userId The user ID to compute the power level for
|
||||||
|
* @param powerLevelEvents The power level event for the room
|
||||||
|
* @param creators The set of creator user IDs for the room if the room is a 'hydra' room, otherwise the empty set.
|
||||||
|
*/
|
||||||
|
function powerLevelForUserId(userId: string, powerLevelEvent: MatrixEvent, creators: Set<string>): number {
|
||||||
|
if (creators.has(userId)) {
|
||||||
|
// As of "Hydra", If the user is a creator, they always have the highest power level
|
||||||
|
return Infinity;
|
||||||
|
} else {
|
||||||
|
const evContent = powerLevelEvent.getDirectionalContent();
|
||||||
|
|
||||||
|
const users: { [userId: string]: number } = evContent.users || {};
|
||||||
|
|
||||||
|
if (users[userId] !== undefined && Number.isInteger(users[userId])) {
|
||||||
|
return users[userId];
|
||||||
|
} else if (evContent.users_default !== undefined) {
|
||||||
|
return evContent.users_default;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -374,7 +374,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
private heroes: Hero[] | null = null;
|
private heroes: Hero[] | null = null;
|
||||||
// flags to stop logspam about missing m.room.create events
|
// flags to stop logspam about missing m.room.create events
|
||||||
private getTypeWarning = false;
|
private getTypeWarning = false;
|
||||||
private getVersionWarning = false;
|
|
||||||
private membersPromise?: Promise<boolean>;
|
private membersPromise?: Promise<boolean>;
|
||||||
|
|
||||||
// XXX: These should be read-only
|
// XXX: These should be read-only
|
||||||
@@ -606,18 +605,10 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the version of the room
|
* Gets the version of the room
|
||||||
* @returns The version of the room, or null if it could not be determined
|
* @returns The version of the room
|
||||||
*/
|
*/
|
||||||
public getVersion(): string {
|
public getVersion(): string {
|
||||||
const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
|
return this.currentState.getRoomVersion();
|
||||||
if (!createEvent) {
|
|
||||||
if (!this.getVersionWarning) {
|
|
||||||
logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event");
|
|
||||||
this.getVersionWarning = true;
|
|
||||||
}
|
|
||||||
return "1";
|
|
||||||
}
|
|
||||||
return createEvent.getContent()["room_version"] ?? "1";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
40
src/utils/roomVersion.ts
Normal file
40
src/utils/roomVersion.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room versions strings that we know about and do not use hydra semantics.
|
||||||
|
*/
|
||||||
|
const HYDRA_ROOM_VERSIONS = ["org.matrix.hydra.11", "12"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given room version is one where new "hydra" power level
|
||||||
|
* semantics (ie. room version 12 or later) should be used
|
||||||
|
* (see https://github.com/matrix-org/matrix-spec-proposals/pull/4289).
|
||||||
|
* This will return `true` for versions that are known to the js-sdk and
|
||||||
|
* use hydra: any room versions unknown to the js-sdk (experimental or
|
||||||
|
* otherwise) will cause the function to return `false`.
|
||||||
|
*
|
||||||
|
* @param roomVersion - The version of the room to check.
|
||||||
|
* @returns `true` if hydra semantics should be used for the room version, `false` otherwise.
|
||||||
|
*/
|
||||||
|
export function shouldUseHydraForRoomVersion(roomVersion: string): boolean {
|
||||||
|
// Future new room versions must obviously be added to the constant above,
|
||||||
|
// otherwise the js-sdk will use the old, pre-hydra semantics. At some point
|
||||||
|
// it would make sense to assume hydra for unknown versions but this will break
|
||||||
|
// any rooms using unknown versions, so at hydra switch time we've agreed all
|
||||||
|
// Element clients will only use hydra for the two specific hydra versions.
|
||||||
|
return HYDRA_ROOM_VERSIONS.includes(roomVersion);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user