|
| 1 | +# MSC4115: membership metadata on events |
| 2 | + |
| 3 | +## Background |
| 4 | + |
| 5 | +Consider the following Event DAG: |
| 6 | + |
| 7 | +```mermaid |
| 8 | +graph BT; |
| 9 | + B[Bob joins]; |
| 10 | + B-->A; |
| 11 | + C-->A; |
| 12 | + D-->B; |
| 13 | + D-->C; |
| 14 | +``` |
| 15 | + |
| 16 | +Bob has joined a room, but at the same time, another user has sent a message |
| 17 | +`C`. |
| 18 | + |
| 19 | +Depending on the configuration of the room, Bob's server may serve the event |
| 20 | +`C` to Bob's client. However, if the room is encrypted, Bob will not be on the |
| 21 | +recipient list for `C` and the sender will not share the message key with Bob, |
| 22 | +even though, in an absolute time reference, `C` may have been sent at a later |
| 23 | +timestamp than Bob's join. |
| 24 | + |
| 25 | +Unfortunately, there is no way for Bob's client to reliably distinguish events |
| 26 | +such as `A` and `C` that were sent "before" he joined (and he should therefore |
| 27 | +not expect to decrypt) from those such as `D` that were sent later. |
| 28 | + |
| 29 | +(Aside: there are two parts to a complete resolution of this "forked-DAG" |
| 30 | +problem. The first part is making sure that the *sender* of an encrypted event |
| 31 | +has a clear idea of who was a member at the point of the event; the second part |
| 32 | +is making sure that the *recipient* knows whether or not they were a member at |
| 33 | +the point of the event and should therefore expect to receive keys for it. This |
| 34 | +MSC deals only with the second part. The whole situation is discussed in more |
| 35 | +detail at https://github.com/element-hq/element-meta/issues/2268.) |
| 36 | + |
| 37 | +A similar scenario can arise even in the absence of a forked DAG: clients |
| 38 | +see events sent when the user was not in the room if the room has [History |
| 39 | +Visibility](https://spec.matrix.org/v1.10/client-server-api/#room-history-visibility) |
| 40 | +set to `shared`. (This is fairly common even in encrypted rooms, partly because |
| 41 | +that is the default state for new rooms even using the `private_chat` preset |
| 42 | +for the [`/createRoom`](https://spec.matrix.org/v1.10/client-server-api/#post_matrixclientv3createroom) |
| 43 | +request, and also because history-sharing solutions such as |
| 44 | +[MSC3061](https://github.com/matrix-org/matrix-spec-proposals/pull/3061) rely |
| 45 | +on it.) |
| 46 | + |
| 47 | +As a partial solution to the forked-DAG problem, which will also solve the |
| 48 | +problem of historical message visibility, we propose a mechanism for servers to |
| 49 | +inform clients of their room membership at each event. |
| 50 | + |
| 51 | +## Proposal |
| 52 | + |
| 53 | +The `unsigned` structure contains data added to an event by a homeserver when |
| 54 | +serving an event over the client-server API. (See |
| 55 | +[specification](https://spec.matrix.org/v1.9/client-server-api/#definition-clientevent)). |
| 56 | + |
| 57 | +We propose adding a new optional property, `membership`. If returned by the |
| 58 | +server, it MUST contain the membership of the user making the request, |
| 59 | +according to the state of the room at the time of the event being returned. If |
| 60 | +the user had no membership at that point (ie, they had yet to join or be |
| 61 | +invited), `membership` is set to `leave`. Any changes caused by the event |
| 62 | +itself (ie, if the event itself is a `m.room.member` event for the requesting |
| 63 | +user) are *included*. |
| 64 | + |
| 65 | +In other words: servers MUST follow the following algorithm when populating |
| 66 | +the `unsigned.membership` property on an event E and serving it to a user Alice: |
| 67 | + |
| 68 | +1. Consider the room state just *after* event E landed (accounting for E |
| 69 | + itself, but not any other events in the DAG which are not ancestors of E). |
| 70 | +2. Within the state, find the event M with type `m.room.member` and `state_key` |
| 71 | + set to Alice's user ID. |
| 72 | +3. * If no such event exists, set `membership` to `leave`. |
| 73 | + * Otherwise, set `membership` to the value of the `membership` property of |
| 74 | + the content of M. |
| 75 | + |
| 76 | +It is recommended that homeservers SHOULD populate the new property wherever |
| 77 | +practical, but they MAY omit it if necessary (for example, if calculating the |
| 78 | +value is expensive, servers might choose to only implement it in encrypted |
| 79 | +rooms). Clients MUST in any case treat the new property as optional. |
| 80 | + |
| 81 | +For the avoidance of doubt, the new `membership` property is added to all |
| 82 | +Client-Server API endpoints that return events, including, but not limited to, |
| 83 | +[`/sync`](https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3sync), |
| 84 | +[`/messages`](https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3roomsroomidmessages), |
| 85 | +[`/state`](https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3roomsroomidstate), |
| 86 | +and deprecated endpoints such as |
| 87 | +[`/events`](https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3events) |
| 88 | +and |
| 89 | +[`/initialSync`](https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3events). |
| 90 | + |
| 91 | + |
| 92 | +Example event including the new property, as seen in the response to a request made by `@user:example.org`: |
| 93 | + |
| 94 | +```json5 |
| 95 | +{ |
| 96 | + "content": { |
| 97 | + "membership": "join" |
| 98 | + }, |
| 99 | + "event_id": "$26RqwJMLw-yds1GAH_QxjHRC1Da9oasK0e5VLnck_45", |
| 100 | + "origin_server_ts": 1632489532305, |
| 101 | + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", |
| 102 | + "sender": "@example:example.org", |
| 103 | + "state_key": "@example:example.org", |
| 104 | + "type": "m.room.member", |
| 105 | + "unsigned": { |
| 106 | + "age": 1567437, |
| 107 | + // @user:example.org's membership at the time this event was sent |
| 108 | + "membership": "leave", |
| 109 | + "redacted_because": { |
| 110 | + "content": { |
| 111 | + "reason": "spam" |
| 112 | + }, |
| 113 | + "event_id": "$Nhl3rsgHMjk-DjMJANawr9HHAhLg4GcoTYrSiYYGqEE", |
| 114 | + "origin_server_ts": 1632491098485, |
| 115 | + "redacts": "$26RqwJMLw-yds1GAH_QxjHRC1Da9oasK0e5VLnck_45", |
| 116 | + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", |
| 117 | + "sender": "@moderator:example.org", |
| 118 | + "type": "m.room.redaction", |
| 119 | + "unsigned": { |
| 120 | + // @user:example.org's membership at the time the redaction was sent |
| 121 | + "membership": "join", |
| 122 | + "age": 1257 |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | +} |
| 127 | +``` |
| 128 | + |
| 129 | +## Potential issues |
| 130 | + |
| 131 | +None foreseen. |
| 132 | + |
| 133 | +## Alternatives |
| 134 | + |
| 135 | +1. https://github.com/element-hq/element-meta/issues/2268#issuecomment-1904069895 |
| 136 | + proposes use of a Bloom filter — or possibly several Bloom filters — to |
| 137 | + mitigate this problem in a more general way. It is the opinion of the author of |
| 138 | + this MSC that there is room for both approaches. |
| 139 | + |
| 140 | +2. We could attempt to calculate the membership state on the client side. This |
| 141 | + might help in a majority of cases, but it will be unreliable in the presence |
| 142 | + of forked DAGs. It would require clients to implement the [state resolution |
| 143 | + algorithm](https://spec.matrix.org/v1.10/rooms/v11/#state-resolution), which |
| 144 | + would be prohibitively complicated for most clients. |
| 145 | + |
| 146 | +## Security considerations |
| 147 | + |
| 148 | +None foreseen. |
| 149 | + |
| 150 | +## Unstable prefix |
| 151 | + |
| 152 | +While this proposal is in development, the name `io.element.msc4115.membership` |
| 153 | +MUST be used in place of `membership`. |
| 154 | + |
| 155 | +## Dependencies |
| 156 | + |
| 157 | +None. |
0 commit comments