check if you are allowed to redact a message
This commit is contained in:
parent
128f9812a6
commit
23459aad52
7 changed files with 144 additions and 8 deletions
|
@ -40,7 +40,7 @@ export class TimelineViewModel extends ViewModel {
|
|||
super(options);
|
||||
const {room, timeline, ownUserId} = options;
|
||||
this._timeline = this.track(timeline);
|
||||
this._tiles = new TilesCollection(timeline.entries, tilesCreator(this.childOptions({room, ownUserId})));
|
||||
this._tiles = new TilesCollection(timeline.entries, tilesCreator(this.childOptions({room, timeline, ownUserId})));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -101,4 +101,8 @@ export class BaseMessageTile extends SimpleTile {
|
|||
redact(reason, log) {
|
||||
return this._room.sendRedaction(this._entry.id, reason, log);
|
||||
}
|
||||
|
||||
get canRedact() {
|
||||
return this._powerLevels.canRedactFromSender(this._entry.sender);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,4 +123,8 @@ export class SimpleTile extends ViewModel {
|
|||
get _room() {
|
||||
return this.getOption("room");
|
||||
}
|
||||
|
||||
get _powerLevels() {
|
||||
return this.getOption("timeline").powerLevels;
|
||||
}
|
||||
}
|
||||
|
|
97
src/matrix/room/timeline/PowerLevels.js
Normal file
97
src/matrix/room/timeline/PowerLevels.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
export class PowerLevels {
|
||||
constructor({powerLevelEvent, createEvent, ownUserId}) {
|
||||
this._plEvent = powerLevelEvent;
|
||||
this._createEvent = createEvent;
|
||||
this._ownUserId = ownUserId;
|
||||
}
|
||||
|
||||
canRedactFromSender(userId) {
|
||||
if (userId === this._ownUserId) {
|
||||
return true;
|
||||
} else {
|
||||
return this.canRedact;
|
||||
}
|
||||
}
|
||||
|
||||
get canRedact() {
|
||||
return this._getUserLevel(this._ownUserId) >= this._getActionLevel("redact");
|
||||
}
|
||||
|
||||
_getUserLevel(userId) {
|
||||
if (this._plEvent) {
|
||||
let userLevel = this._plEvent.content?.users?.[userId];
|
||||
if (typeof userLevel !== "number") {
|
||||
userLevel = this._plEvent.content?.users_default;
|
||||
}
|
||||
if (typeof userLevel === "number") {
|
||||
return userLevel;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else if (this._createEvent) {
|
||||
if (userId === this._createEvent.content?.creator) {
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @param {string} action either "invite", "kick", "ban" or "redact". */
|
||||
_getActionLevel(action) {
|
||||
const level = this._plEvent?.content[action];
|
||||
if (typeof level === "number") {
|
||||
return level;
|
||||
} else {
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function tests() {
|
||||
const alice = "@alice:hs.tld";
|
||||
const bob = "@bob:hs.tld";
|
||||
const createEvent = {content: {creator: alice}};
|
||||
const powerLevelEvent = {content: {
|
||||
redact: 50,
|
||||
users: {
|
||||
[alice]: 50
|
||||
},
|
||||
users_default: 0
|
||||
}};
|
||||
|
||||
return {
|
||||
"redact somebody else event with power level event": assert => {
|
||||
const pl1 = new PowerLevels({powerLevelEvent, ownUserId: alice});
|
||||
assert.equal(pl1.canRedact, true);
|
||||
const pl2 = new PowerLevels({powerLevelEvent, ownUserId: bob});
|
||||
assert.equal(pl2.canRedact, false);
|
||||
},
|
||||
"redact somebody else event with create event": assert => {
|
||||
const pl1 = new PowerLevels({createEvent, ownUserId: alice});
|
||||
assert.equal(pl1.canRedact, true);
|
||||
const pl2 = new PowerLevels({createEvent, ownUserId: bob});
|
||||
assert.equal(pl2.canRedact, false);
|
||||
},
|
||||
"redact own event": assert => {
|
||||
const pl = new PowerLevels({ownUserId: alice});
|
||||
assert.equal(pl.canRedactFromSender(alice), true);
|
||||
assert.equal(pl.canRedactFromSender(bob), false);
|
||||
},
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ import {Direction} from "./Direction.js";
|
|||
import {TimelineReader} from "./persistence/TimelineReader.js";
|
||||
import {PendingEventEntry} from "./entries/PendingEventEntry.js";
|
||||
import {RoomMember} from "../members/RoomMember.js";
|
||||
import {PowerLevels} from "./PowerLevels.js";
|
||||
|
||||
export class Timeline {
|
||||
constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock}) {
|
||||
|
@ -40,11 +41,15 @@ export class Timeline {
|
|||
});
|
||||
this._readerRequest = null;
|
||||
this._allEntries = null;
|
||||
this._powerLevels = null;
|
||||
}
|
||||
|
||||
/** @package */
|
||||
async load(user, membership, log) {
|
||||
const txn = await this._storage.readTxn(this._timelineReader.readTxnStores.concat(this._storage.storeNames.roomMembers));
|
||||
const txn = await this._storage.readTxn(this._timelineReader.readTxnStores.concat(
|
||||
this._storage.storeNames.roomMembers,
|
||||
this._storage.storeNames.roomState
|
||||
));
|
||||
const memberData = await txn.roomMembers.get(this._roomId, user.id);
|
||||
if (memberData) {
|
||||
this._ownMember = new RoomMember(memberData);
|
||||
|
@ -66,6 +71,22 @@ export class Timeline {
|
|||
} finally {
|
||||
this._disposables.disposeTracked(readerRequest);
|
||||
}
|
||||
this._powerLevels = await this._loadPowerLevels(txn);
|
||||
}
|
||||
|
||||
async _loadPowerLevels(txn) {
|
||||
const powerLevelsState = await txn.roomState.get(this._roomId, "m.room.power_levels", "");
|
||||
if (powerLevelsState) {
|
||||
return new PowerLevels({
|
||||
powerLevelEvent: powerLevelsState.event,
|
||||
ownUserId: this._ownMember.userId
|
||||
});
|
||||
}
|
||||
const createState = await txn.roomState.get(this._roomId, "m.room.create", "");
|
||||
return new PowerLevels({
|
||||
createEvent: createState.event,
|
||||
ownUserId: this._ownMember.userId
|
||||
});
|
||||
}
|
||||
|
||||
_setupEntries(timelineEntries) {
|
||||
|
@ -199,7 +220,12 @@ export class Timeline {
|
|||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
enableEncryption(decryptEntries) {
|
||||
this._timelineReader.enableEncryption(decryptEntries);
|
||||
}
|
||||
|
||||
get powerLevels() {
|
||||
return this._powerLevels;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,21 +17,26 @@ limitations under the License.
|
|||
|
||||
import {MAX_UNICODE} from "./common.js";
|
||||
|
||||
function encodeKey(roomId, eventType, stateKey) {
|
||||
return `${roomId}|${eventType}|${stateKey}`;
|
||||
}
|
||||
|
||||
export class RoomStateStore {
|
||||
constructor(idbStore) {
|
||||
this._roomStateStore = idbStore;
|
||||
}
|
||||
|
||||
async getAllForType(type) {
|
||||
getAllForType(roomId, type) {
|
||||
throw new Error("unimplemented");
|
||||
}
|
||||
|
||||
async get(type, stateKey) {
|
||||
throw new Error("unimplemented");
|
||||
get(roomId, type, stateKey) {
|
||||
const key = encodeKey(roomId, type, stateKey);
|
||||
return this._roomStateStore.get(key);
|
||||
}
|
||||
|
||||
async set(roomId, event) {
|
||||
const key = `${roomId}|${event.type}|${event.state_key}`;
|
||||
set(roomId, event) {
|
||||
const key = encodeKey(roomId, event.type, event.state_key);
|
||||
const entry = {roomId, event, key};
|
||||
return this._roomStateStore.put(entry);
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ export class BaseMessageView extends TemplateView {
|
|||
const options = [];
|
||||
if (vm.isPending) {
|
||||
options.push(Menu.option(vm.i18n`Cancel`, () => vm.abortSending()));
|
||||
} else if (vm.shape !== "redacted") {
|
||||
} else if (vm.shape !== "redacted" && vm.canRedact) {
|
||||
options.push(Menu.option(vm.i18n`Delete`, () => vm.redact()).setDestructive());
|
||||
}
|
||||
return options;
|
||||
|
|
Reference in a new issue