Skip to content

Commit

Permalink
Simplify MatrixClient::setPowerLevel API (#3570)
Browse files Browse the repository at this point in the history
* Simplify `MatrixClient::setPowerLevel` API

While making it more resilient to causing issues like nuking room state

* Handle edge case

* Fix tests

* Add test coverage
  • Loading branch information
t3chguy authored Jul 11, 2023
1 parent 5df4eba commit d2b782a
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 22 deletions.
82 changes: 74 additions & 8 deletions spec/integ/matrix-client-methods.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1386,23 +1386,89 @@ describe("MatrixClient", function () {
expectation: {},
},
])("should modify power levels of $userId correctly", async ({ userId, powerLevel, expectation }) => {
const event = {
getType: () => "m.room.power_levels",
getContent: () => ({
users: {
"alice@localhost": 50,
httpBackend!.when("GET", "/state/m.room.power_levels/").respond(200, {
users: {
"alice@localhost": 50,
},
});

httpBackend!
.when("PUT", "/state/m.room.power_levels")
.check((req) => {
expect(req.data.users).toStrictEqual(expectation);
})
.respond(200, {});

const prom = client!.setPowerLevel("!room_id:server", userId, powerLevel);
await httpBackend!.flushAllExpected();
await prom;
});

it("should use power level from room state if available", async () => {
client!.clientRunning = true;
client!.isInitialSyncComplete = () => true;
const room = new Room("!room_id:server", client!, client!.getUserId()!);
room.currentState.events.set("m.room.power_levels", new Map());
room.currentState.events.get("m.room.power_levels")!.set(
"",
new MatrixEvent({
type: "m.room.power_levels",
state_key: "",
content: {
users: {
"@bob:localhost": 50,
},
},
}),
} as MatrixEvent;
);
client!.getRoom = () => room;

httpBackend!
.when("PUT", "/state/m.room.power_levels")
.check((req) => {
expect(req.data.users).toStrictEqual(expectation);
expect(req.data).toStrictEqual({
users: {
"@bob:localhost": 50,
[userId]: 42,
},
});
})
.respond(200, {});

const prom = client!.setPowerLevel("!room_id:server", userId, 42);
await httpBackend!.flushAllExpected();
await prom;
});

it("should throw error if state API errors", async () => {
httpBackend!.when("GET", "/state/m.room.power_levels/").respond(500, {
errcode: "ERR_DERP",
});

const prom = client!.setPowerLevel("!room_id:server", userId, 42);
await Promise.all([
expect(prom).rejects.toMatchInlineSnapshot(`[ERR_DERP: MatrixError: [500] Unknown message]`),
httpBackend!.flushAllExpected(),
]);
});

it("should not throw error if /state/ API returns M_NOT_FOUND", async () => {
httpBackend!.when("GET", "/state/m.room.power_levels/").respond(404, {
errcode: "M_NOT_FOUND",
});

httpBackend!
.when("PUT", "/state/m.room.power_levels")
.check((req) => {
expect(req.data).toStrictEqual({
users: {
[userId]: 42,
},
});
})
.respond(200, {});

const prom = client!.setPowerLevel("!room_id:server", userId, powerLevel, event);
const prom = client!.setPowerLevel("!room_id:server", userId, 42);
await httpBackend!.flushAllExpected();
await prom;
});
Expand Down
49 changes: 35 additions & 14 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ import * as ContentHelpers from "./content-helpers";
import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
import { Room, NotificationCountType, RoomEvent, RoomEventHandlerMap, RoomNameState } from "./models/room";
import { RoomMemberEvent, RoomMemberEventHandlerMap } from "./models/room-member";
import { RoomStateEvent, RoomStateEventHandlerMap } from "./models/room-state";
import { IPowerLevelsContent, RoomStateEvent, RoomStateEventHandlerMap } from "./models/room-state";
import {
IAddThreePidOnlyBody,
IBindThreePidBody,
Expand Down Expand Up @@ -4256,24 +4256,48 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa

/**
* Set a power level to one or multiple users.
* Will apply changes atop of current power level event from local state if running & synced, falling back
* to fetching latest from the `/state/` API.
* @param roomId - the room to update power levels in
* @param userId - the ID of the user or users to update power levels of
* @param powerLevel - the numeric power level to update given users to
* @param event - deprecated and no longer used.
* @returns Promise which resolves: to an ISendEventResponse object
* @returns Rejects: with an error response.
*/
public setPowerLevel(
public async setPowerLevel(
roomId: string,
userId: string | string[],
powerLevel: number | undefined,
event: MatrixEvent | null,
/**
* @deprecated no longer needed, unused.
*/
event?: MatrixEvent | null,
): Promise<ISendEventResponse> {
let content = {
users: {} as Record<string, number>,
};
if (event?.getType() === EventType.RoomPowerLevels) {
// take a copy of the content to ensure we don't corrupt
// existing client state with a failed power level change
content = utils.deepCopy(event.getContent());
let content: IPowerLevelsContent | undefined;
if (this.clientRunning && this.isInitialSyncComplete()) {
content = this.getRoom(roomId)?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
}
if (!content) {
try {
content = await this.getStateEvent(roomId, EventType.RoomPowerLevels, "");
} catch (e) {
// It is possible for a Matrix room to not have a power levels event
if (e instanceof MatrixError && e.errcode === "M_NOT_FOUND") {
content = {};
} else {
throw e;
}
}
}

// take a copy of the content to ensure we don't corrupt
// existing client state with a failed power level change
content = utils.deepCopy(content);

if (!content?.users) {
content.users = {};
}
const users = Array.isArray(userId) ? userId : [userId];
for (const user of users) {
if (powerLevel == null) {
Expand All @@ -4283,10 +4307,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}

const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", {
$roomId: roomId,
});
return this.http.authedRequest(Method.Put, path, undefined, content);
return this.sendStateEvent(roomId, EventType.RoomPowerLevels, content, "");
}

/**
Expand Down

0 comments on commit d2b782a

Please sign in to comment.