-
Notifications
You must be signed in to change notification settings - Fork 385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MSC3531: Letting moderators hide messages pending moderation #3531
base: main
Are you sure you want to change the base?
Changes from all commits
8f3e726
9db1e0a
a0205ed
69fdd50
43a2e09
9d6f372
412d62f
726bf18
86b32d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
|
||
# **MSC3531: Letting moderators hide messages pending review** | ||
|
||
Matrix supports **redacting** messages as a mechanism to remove unwanted | ||
content. **Redacting** events, as defined in the Matrix spec, is a mechanism that | ||
entirely removes the content of an event. Users can redact their own events, and | ||
room moderators can redact unwanted events, including illegal content. | ||
At present, there is no manner of **undoing** these redactions. | ||
|
||
Historically, redacting messages has been useful for two use cases: | ||
|
||
1. a user accidentally posting a password, credit card number or other confidential information, | ||
in which case the information must be scrubbed as fast as possible from all places; | ||
2. a moderator removing spam, bullying, etc. from a malicious user / spam bot. | ||
|
||
Experience shows that redacting messages for case 2. is not always the best solution: | ||
|
||
1. moderators make mistakes and there is currently no way for them to fix these; | ||
2. in many cases, it may be desirable for a moderator to quickly hide a message | ||
before having a conversation with other moderators to determine whether the | ||
message should be let through (e.g. discussing whether the local room's CoC | ||
should allow a possibly inflamatory political message - or a newbie moderator | ||
waiting for experienced moderators to come online to ask them for clarifications | ||
on borderline content); | ||
3. some bots automatically remove messages based on heuristics (e.g. users sending | ||
too many messages or too many images) but may get it wrong, in which case the | ||
moderator currently cannot fix the errors of these bots. | ||
|
||
In addition, proposals such as MSC3215, which aims to decentralize moderation, | ||
will very likely increase the number of moderators - and in particular, the | ||
number of moderators who may not be familiar with moderation tools, hence will | ||
make mistakes. | ||
|
||
For all these reasons, it would be very useful to have a mechanism that would | ||
let moderators undo their own redactions. Unfortunately, reversing a redaction | ||
is tricky, as we cover in the **Alternatives** section. | ||
|
||
Rather, we propose a spec to let moderators *hide messages pending review*. This | ||
mechanism is entirely client-based and will not prevent hidden messages | ||
from being distributed, only from being seen by non-moderator users. This spec | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIUC this won't prevent hidden messages from being seen by non-moderator users. It will just provide enough information such that clients may hide them. Basically this does not hide messages from a motivated non-moderator user but it is expected that cooperative clients don't see the messages (once they are aware of the hiding). |
||
can then be used by clients or bots such as Mjölnir to implement two phase | ||
redaction: | ||
1. a first phase during which messages are flagged for moderation (either by | ||
a bot or manually) and hidden from general consumption; | ||
1. a second phase during which moderators may either restore the message or | ||
`redact` it entirely. | ||
|
||
## **Proposal** | ||
We introduce a new type of event: `m.visibility`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is too confusing with the existing
All of which the average developer would expect this to be https://spec.matrix.org/latest/client-server-api/#room-history-visibility - please change your terminology to avoid confusing developers. Possible suggestions of non-overloaded terms:
|
||
|
||
Events with type `m.visibility` are ignored by clients if they are invalid or sent by users with | ||
a powerlevel insufficient to send a *state event* `m.visibility`. This relation controls whether *clients* should | ||
display an event or hide it. | ||
Comment on lines
+51
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this could use an explicit mention, that this is NOT sent as a state event, even though the power level check to apply it uses a state event PL. Tbh, I would probably prefer to introduce a top level powerlevel like for redactions for this, since this would be quite irregular otherwise. |
||
|
||
|
||
An event of `m.visibility` MUST with the following *content* fields: | ||
|
||
| Content Key | Type | Description | | ||
|----------------|---------|-------------| | ||
| `m.relates_to` | Visibility Relation | **Required** The payload for this event | | ||
| `visible` | `boolean` | **Required** If `true`, clients should show the affected event normally. If false, clients should mark the affected event as hidden pending review. | | ||
| `reason` | `string` | Optional. If `visible` is `false`, a reason that clients MAY display to indicate why the affected event is hidden pending review. | | ||
|
||
Visibility relation | ||
|
||
| Content Key | Type | Description | | ||
|----------------|-----------|-------------| | ||
| `rel_type` | `string` | **Required** Must be `"m.reference"` | | ||
| `event_id` | `eventId` | **Required** eventId of the event affected by this visibility change. Must be a past event in this room. | | ||
|
||
### Server behavior | ||
|
||
No changes in server behavior. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without server changes, this feels very half-baked because it's just a best-effort hiding process. This may be unacceptable to users and create a bad UX if I hide my sensitive message only to realise later that it's visible to other users because they aren't using Element. That being said, basically any kind of hiding process like this is basically impossible to guarantee, like unsending emails, it's just whether or not this will occur frequently enough to impact users experience or not. |
||
|
||
### Client behavior | ||
|
||
1. When a client receives an event `event` with type `m.visibility` | ||
relating to an existing event `original_event` in room `room`: | ||
1. If the `event` is well-formed and powerlevel of `event.sender` in `room` is greater or equal | ||
to the powerlevel needed to sent **state event** `m.visibility` | ||
1. If `event` specifies a visibility of "hidden", mark `original_event` as hidden | ||
1. In every display of `original_event`, either by itself or in a reaction | ||
1. If the current user is the sender of `original_event` | ||
1. Label the display of `original_event` with a label such as `(pending moderation)` | ||
1. If `event.content` contains a string field `reason`, this field may be used to display a reason for moderation. | ||
1. Otherwise, if the current user has a powerlevel greater or | ||
equal to `m.visibility` | ||
1. Display `original_event` as a spoiler. | ||
1. Label the display of `original_event` with a label such as `(pending moderation)` | ||
1. If `event.content` contains a string field `reason`, this field may be used to display a reason for moderation. | ||
1. Otherwise | ||
1. Instead of displaying `original_event`, display a message such as `Message is pending moderation` | ||
1. If `event.content` contains a string field `reason`, this field may be used to display a reason for moderation. | ||
1. Otherwise, if `event` specifies a visibility of "visible", mark `original_event` as visible | ||
1. Display `original_event` exactly as it would be displayed without this MSC | ||
1. Otherwise, ignore | ||
1. Otherwise, ignore | ||
1. When a client prepares to display a message `original_event` with visibility "hidden", whether by itself or in a reaction | ||
1. (see 1.1.1.1.1. for details on how to display `original_event`) | ||
1. If an event `event` with `rel_type` `m.visibility` and relating to an existing event `original_event` is redacted, update the display or `original_event` as per the latest event with `rel_type` `m.visibility` in this room relating to the same `original_event`. | ||
|
||
If several reactions race against each other to mark a message as visible or | ||
hidden, we consider the most recent one (by order of `origin_server_ts`) the | ||
source of truth. | ||
|
||
For simplicity, if a user gains or loses the powerlevel `m.visibility`, this | ||
does **not** affect any of the `m.visibility` relations already sent by that user. | ||
This may, however, affect how hidden events are displayed to this specific user. | ||
|
||
### Example use | ||
|
||
A moderation bot such as Mjölnir might implement two-phase redaction as follows: | ||
1. When a room protection rule or a moderator requires Mjölnir to redact a | ||
message `original_message` in `room` | ||
1. Copy `original_message` to a "moderation pending" room as message `backup_message`, with some UX to | ||
decide whether `backup_message` should be PASS or REJECT. | ||
1. Mark `original_message` in `room` as hidden, using the current MSC. | ||
1. When a moderator marks `backup_message` as PASS | ||
1. Mark `original_message` in `room` as visible, using the current MSC. | ||
1. Remove `backup_message` from the "moderation pending" room. | ||
1. When a moderator marks clone `backup_message` as REJECT | ||
1. Send a message `m.room.redaction` to `room` to fully redact message `original_message`. | ||
1. Remove `backup_message` from the "moderation pending" room. | ||
1. If, after <some retention duration, e.g. one week>, a clone `backup_message` has been | ||
marked neither PASS nor REJECT | ||
1. Behave as if `backup_message` had been marked REJECT | ||
|
||
## Potential issues | ||
### Abuse by moderators | ||
This proposal does not give substantial new powers to moderators, so we don't | ||
think that there is cause for concern here. | ||
|
||
### Race conditions | ||
There may be race conditions between e.g. an edition (https://github.com/matrix-org/matrix-doc/pull/2676) and marking a message visible/hidden. We do not think that this can cause any real issue. | ||
|
||
### Hidden channel | ||
As messages are hidden but still distributed to all clients in the room, it is | ||
entirely possible to write a client/bot that ignores hiding and one could | ||
imagine using hidden messages to semi-covertly exchange messages in a room. | ||
|
||
As there are already countless ways to implement this, we don't foresee this to | ||
cause any problem. | ||
Yoric marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### Liabilities | ||
|
||
It is possible that, in some countries, if moderators decide to mark content as | ||
hidden but fail to redact it, this could make the homeserver owner legally | ||
responsible for illegal content being exchanged through this covert channel. | ||
|
||
We believe that using a bot that automatically redacts hidden messages after a | ||
retention period would help administrators avoid such liabilities. | ||
|
||
## Alternatives | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have no idea why this doesn't mention what I see is the obvious solution here: make them state events. E.g: {
type: "m.pending_moderation",
state_key: "@user-who-wants-to-moderate",
content: {
hidden: [ "$aaa", "$bbb", "$ccc" ]
}
} This has numerous benefits over having them as state events:
I haven't fully thought this through, but it seems to be a more expressive and reliable solution to this problem that this proposal. Please add it to the Alternatives section. |
||
### Server behavior changes | ||
|
||
We could amend this proposal to have the server reject messages with type | ||
`m.visibility` if these messages are sent by a user with a powerlevel below | ||
`m.visibility`. However, this would require changes to the flow of encryption | ||
to let the server read the relation between events, something that is less than | ||
ideal. | ||
|
||
We prefer requiring that clients ignore messages sent by users without a sufficient | ||
powerlevel. | ||
|
||
### A message to undo a redaction | ||
|
||
As the original objective of this proposal is to undo redactions, one could | ||
imagine a message `m.room.undo_redaction` with the following behavior: | ||
|
||
* The ability to send a `m.room.undo_redaction` is controlled by a | ||
powerlevel, just as `m.room.redaction`. | ||
* When a server receives a `m.room.undo_redaction` for event E, event E loses | ||
its "redacted" status, in particular in any future `sync` or | ||
`/room/.../event/...` or other, the original event E is returned, rather | ||
than its redacted status. | ||
* When a client receives a `m.room.undo_redaction` for an event E, they need | ||
to refetch event E from the homeserver. | ||
|
||
This proposal would have the benefit of removing the hidden channel. | ||
|
||
However, servers are intended to redact events immediately and permanently, though | ||
regulations for some areas of operation require the contents to be preserved for a | ||
short amount of time. In any case, it is not possible to determine | ||
how long a server is willing or able to keep event contents, so we can only assume | ||
it has not kept them at all. Any attempt to undo redaction would, at best, race | ||
against this retention duration, which may differ across homeservers in the same | ||
room, and might end up causing divergence between the room views. | ||
|
||
Thus, undoing is not possible, in practice. | ||
|
||
### Injecting content in redacted messages | ||
An alternative mechanism to undo redactions would be to let moderators un-redact | ||
a message by injecting new content in it. This would let clients or moderation | ||
bots such as Mjölnir implement undoing redactions by first backing up redacted | ||
messages (in a manner similar to what we discuss in "Example use"), then if a | ||
redaction is canceled, reinjecting content. We decided not to pursue this | ||
mechanism as it is more complicated and it opens abuse vectors by malicious | ||
moderators de facto modifying the content of other user's messages (even if this | ||
could be mitigated by clients displaying who has modified a user's messages). | ||
|
||
### Letting users hide their own messages | ||
|
||
There would be use cases for users hiding their own messages, e.g. marking a | ||
task as complete. We believe that this complicates the present MSC, as it | ||
introduces edge cases that deserve their own discussion, e.g.: | ||
|
||
- can a moderator make a message hidden by a user visible? | ||
- how do we reinterpret a sequence of visibility change messages interleaving | ||
self-hide, self-unhide, moderator-hide, moderator-unhide when one or more | ||
of the messages in the sequence gets redacted? | ||
|
||
For these reasons, we prefer postponing such feature to a further MSC. | ||
|
||
## Security considerations | ||
### Old clients | ||
|
||
Old clients that do not implement this MSC will continue displaying messages that | ||
should be hidden. We believe that it's an acceptable risk, as it does not expose | ||
data that is meant to be kept private. | ||
|
||
## Unstable prefix | ||
|
||
During the prototyping phase: | ||
|
||
- message type `m.visibility` should be prefixed into | ||
`org.matrix.msc3531.visibility`. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit baffled by the fact nobody talks about users hiding their own messages. (Or maybe it is implied by the implementation of the mechanism and I missed it, in that case I'd like to have it noted explicitly). There are a few valid use cases to hide own messages and I see no reason to limit this to moderators only. For reference, GitHub has a pretty good message hiding feature.
Speaking of GitHub, they enumerate the following possible reasons for hiding a message: Spam, Abuse, Off Topic, Outdated, Duplicate, Resolved. I'd suggest putting most of these as "presets" while allowing custom fields as well (this also rises the question about translation btw).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain what would be the use case for a user hiding their own message, that isn't already covered by users redacting their own message (if the user sent a message but doesn't want it shown any more), or using the <details> tag (if the user sends a long message that they don't want to take a lot of space unless someone is interested in reading the whole thing)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be useful in cases where the user wants to retract a message without redacting it, so that it can still be read by others. A common usage for this on GitHub is to mark messages as resolved.
More generally though, this is about symmetry: users can redact their messages, moderators can redact others' messages. If moderators can hide others' messages, why should users not be able to hide their own?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea a lot.
The main difficulty is that it introduces a few corner cases that are a bit tougher to specify:
For the time being, I'd probably prefer leaving the MSC as is and introducing self-hiding as a followup.
Clarifying this position in Alternatives.