You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
matrix-spec-proposals/proposals/4115-membership-on-events.md

5.6 KiB

MSC4115: membership metadata on events

Background

Consider the following Event DAG:

graph BT;
    B[Bob joins];
    B-->A;
    C-->A;
    D-->B;
    D-->C;

Bob has joined a room, but at the same time, another user has sent a message C.

Depending on the configuration of the room, Bob's server may serve the event C to Bob's client. However, if the room is encrypted, Bob will not be on the recipient list for C and the sender will not share the message key with Bob, even though, in an absolute time reference, C may have been sent at a later timestamp than Bob's join.

Unfortunately, there is no way for Bob's client to reliably distinguish events such as A and C that were sent "before" he joined (and he should therefore not expect to decrypt) from those such as D that were sent later. This situation is discussed in more detail at https://github.com/element-hq/element-meta/issues/2268.

A similar scenario can arise even in the absence of a forked DAG: clients see events sent when the user was not in the room if the room has History Visibility set to shared. (This is fairly common even in encrypted rooms, partly because that is the default state for new rooms even using the private_chat preset for the /createRoom request, and also because history-sharing solutions such as MSC3061 rely on it.

As a partial solution to the forked-DAG problem, which will also solve the problem of historical message visibility, we propose a mechanism for servers to inform clients of their room membership at each event.

Proposal

The unsigned structure contains data added to an event by a homeserver when serving an event over the client-server API. (See specification).

We propose adding a new optional property, membership. If returned by the server, it should contain the membership of the user making the request, according to the state of the room at the time of the event being returned. If the user had no membership at that point (ie, they had yet to join or be invited), membership is set to leave. Any changes caused by the event itself (ie, if the event itself is a m.room.member event for the requesting user) are included.

In other words: servers should follow the following algorithm when populating the unsigned.membership property on an event E and serving it to a user Alice:

  1. Consider the room state just after event E landed (accounting for E itself, but not any other events in the DAG which are not ancestors of E).
  2. Within the state, find the event M with type m.room.member and state_key set to Alice's user ID.
    • If no such event exists, set membership to leave.
    • Otherwise, set membership to the value of the membership property of the content of M.

It is recommended that homeservers populate the new property wherever practical, but they may omit it if necessary (for example, if calculating the value is expensive, servers might choose to only implement it in encrypted rooms). Clients must in any case treat the new property as optional.

For the avoidance of doubt, the new membership property is added to all Client-Server API endpoints that return events, including /sync, /messages, /state, and deprecated endpoints such as /events and /initialSync.

Example event including the new property:

{
  "content": {
    "membership": "join"
  },
  "event_id": "$26RqwJMLw-yds1GAH_QxjHRC1Da9oasK0e5VLnck_45",
  "origin_server_ts": 1632489532305,
  "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
  "sender": "@example:example.org",
  "state_key": "@user:example.org",
  "type": "m.room.member",
  "unsigned": {
    "age": 1567437,
    "membership": "leave",
    "redacted_because": {
      "content": {
        "reason": "spam"
      },
      "event_id": "$Nhl3rsgHMjk-DjMJANawr9HHAhLg4GcoTYrSiYYGqEE",
      "origin_server_ts": 1632491098485,
      "redacts": "$26RqwJMLw-yds1GAH_QxjHRC1Da9oasK0e5VLnck_45",
      "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
      "sender": "@moderator:example.org",
      "type": "m.room.redaction",
      "unsigned": {
        "membership": "leave",
        "age": 1257
      }
    }
  }
}

Potential issues

  • Depending on server implementation, it may be expensive or difficult to implement? Feedback welcome from HS authors on this point.

Alternatives

  1. https://github.com/element-hq/element-meta/issues/2268#issuecomment-1904069895 proposes use of a Bloom filter — or possibly several Bloom filters — to mitigate this problem in a more general way. It is the opinion of the author of this MSC that there is room for both approaches.

  2. We could attempt to calculate the membership state on the client side. This might help in a majority of cases, but it will be unreliable in the presence of forked DAGs, state resolution, etc.

Security considerations

None foreseen.

Unstable prefix

While this proposal is in development, the name io.element.msc4115.membership should be used in place of membership.

Dependencies

None.