Make per-room/per-space profiles more first-class

This completely reworks MSC3189.

Signed-off-by: Robin Townsend <robin@robin.town>
pull/3189/head
Robin Townsend 3 years ago
parent 96c8c036c4
commit fb6c4a2a89

@ -1,128 +0,0 @@
# MSC3189: Per-room/per-space profile data
People frequently have different identities in different communities. In the
context of Matrix, users may therefore want their display name or avatar to
appear differently in certain social contexts, such as within a room or a space.
While most clients technically already support per-room display names (by
getting profile data from a user's membership events in a room), this feature
suffers from a lack of documentation and server-side support. This proposal
attempts to improve on per-room/per-space profile data in the following ways:
1. Documenting the current de-facto mechanism for per-room profile data
2. Keeping global profile changes from overwriting per-room profile data (if desired)
3. Allowing clients to set profile data for an entire space in a single request rather than sending membership events in bulk
## Proposal
### Per-room profile data
First, the existing behavior: When showing a user's display name or avatar in a
room, clients should reference the `displayname` and `avatar_url` attributes of
the user's `m.room.member` state. Thus, to set a display name or avatar in a
specific room, clients should modify these attributes via the relevant state
APIs.
In order to prevent per-room profile data from being overwritten when the user
updates their global profile, an optional query parameter named `force` of type
`boolean` is added to the `PUT /_matrix/client/r0/profile/{userId}/avatar_url`
and `PUT /_matrix/client/r0/profile/{userId}/displayname` endpoints.
If `force` is `true`, the profile change is propagated to all of the user's
rooms by adding, updating, or removing the relevant attribute of the user's
`m.room.member` state (and only that attribute) as needed. Unlike the current
behavior, updating `displayname` *must not* cause the user's `avatar_url` to
change in any rooms, and vice versa.
If `force` is `false` (the default value), the profile change is only propagated
to rooms in which the relevant attribute (`displayname` or `avatar_url`) is
equal to that of the user's global profile before the update. This ensures that
by default, custom per-room profile data will not be overwritten.
### Per-space profile data
Per-space profile data is communicated in the same way as global and per-room
profile data, by updating the relevant `m.room.member` attributes in the
space-room and all of its children, recursively. To make this a simple operation
for clients, another optional query parameter named `space` of type `string` is
added to the `PUT /_matrix/client/r0/profile/{userId}/avatar_url` and `PUT
/_matrix/client/r0/profile/{userId}/displayname` endpoints.
If specified, `space` must be a valid ID of a room of which the user is a member
(regardless of whether it is of type `m.space`<sup id="a1">[1](#f1)</sup>), and
its effect is to limit the scope of the profile change to the given space. This
is achieved by first updating the per-room profile data for the given
space-room, and then recursing into all `m.space.child` rooms of which the user
is a member.
The `space` parameter obeys `?force=false` as well, by only overwriting an
`m.room.member` attribute if it matches the previous profile data of the root
space. If this is not the case, meaning a room with a different per-room profile
has been found, the profile change stops there and does not continue recursing
into the room's space children. Additionally, servers must take care to handle
cycles in the space graph and not recurse infinitely (e.g. by tracking which
rooms it has visited).
## Potential issues
This proposal assumes that having "one true display name per room" is a
desirable feature, since it minimizes complexity for clients and is compatible
with how most implementations already determine profile data. However, since
rooms can belong to multiple spaces, possibly with conflicting profile data,
this causes a certain degree of arbitrariness in what profile data gets set for
such rooms (depending purely on the order in which the user sets their per-space
profiles, and whether `force` is set). If this matters, users can always drill
down to room-level profile settings, though, and clients may assist them e.g. by
displaying a list of applicable per-space profiles to switch between.
Arguably, per-space profile data should be a more first-class feature, with
server support for things like inheriting profile data from parent spaces on
join. This proposal leaves it up to clients to implement such "inheritance"
behavior as they see fit, by altering individual `m.room.member` states when
rooms are joined, added to spaces, etc. If desired, servers could be changed to
automate some of this behavior in the future, though arguably this should be
left to clients, since they have more context for e.g. which parent space the
user was viewing a room from when they joined it.
Finally, there is a pre-existing issue with the profile APIs: If the server has
to propagate a profile change to a large number of rooms, it could take a long
time, and the client could easily time out, potentially leaving some rooms
without the updated profile data. This is nothing new, but this proposal would
make it an even more broken state, since future attempts at changing profile
data without `?force=true` would interpret the rooms that weren't updated as all
having custom per-room profiles. It is expected that a future MSC will address
this by making the profile APIs non-blocking.
## Alternatives
An alternative would be to store per-room/per-space profile data as a part of
[extensible profiles](https://github.com/matrix-org/matrix-doc/pull/1769),
essentially keeping a public mapping of room IDs → profile data in a single
location. While altering `m.room.member` state gives us per-room and per-space
profile data for free, this alternative would require more action from clients
to implement. It would also leak data about users' profiles in private rooms,
which is a significant privacy concern, and it is unclear how conflicting
profiles would affect the "one source of truth" given by `m.room.member` state.
Furthermore, extensible profiles seem unlikely to land anytime soon, while
per-room/per-space profile data is arguably a more urgent feature, and should
not depend on it.
## Security considerations
None that I am aware of.
## Unstable prefix
During development of this feature the versions of the profile APIs augmented
with `force` and `space` will be available at unstable endpoints:
```text
PUT /_matrix/client/unstable/org.matrix.msc3189/profile/{userId}/avatar_url
PUT /_matrix/client/unstable/org.matrix.msc3189/profile/{userId}/displayname
```
## Footnotes
<a id="f1"/>[1]: Room type is ignored primarily to be consistent with how the
space summary API handles room IDs, and also since rooms can technically have
`m.space.child` rooms without being of type `m.space` themselves. [](#a1)

@ -0,0 +1,159 @@
# MSC3189: Per-room/per-space profiles
People frequently have different identities in different communities. In the
context of Matrix, users may therefore want their display name or avatar to
appear differently in certain social contexts, such as within a room or a space.
While most clients technically already support per-room display names (by
getting profile data from a user's membership events in a room), this feature
is undocumented and lacks server-side support. Thus, this proposal introduces a
more robust concept of per-room/per-space profiles along with a dedicated API to
manage them.
## Proposal
First, the existing behavior: When showing a user's display name or avatar in a
room, clients should reference the `displayname` and `avatar_url` attributes of
the user's `m.room.member` state. In the past, some clients have taken advantage
of this behavior by modifying `m.room.member` state directly in order to set
per-room profiles, however this is deprecated in favor of the more robust system
of profile management proposed in this MSC. As such, clients must be aware that
direct changes to `m.room.member` state may be overwritten without warning.
### Profile scope and inheritance
While `m.room.member` state in individual rooms is sufficient for communicating
how users should appear, we would like per-room/per-space profiles to be a more
first-class concept, so that managing them is simple for clients. For this
purpose, an optional query parameter `scope` which takes a room ID is added to
all `/_matrix/client/r0/profile` endpoints. When specified, `scope` changes the
profile endpoints to interact not with the user's global profile, but with the
profile specific to the given space/room.
With regards to profile data, a space/room can be in one of two states: either
it has a custom profile set via the above `scope` API, in which case it is known
as a *profile root*, or it *inherits* its profile data from another profile root
(i.e. one of its ancestor spaces or the user's global profile). By default, all
rooms and spaces are set to inherit from the global profile. Thus, when profile
updates happen, the server uses this inheritance data to determine which rooms
the update affects, and updates their `m.room.member` state accordingly.
Inheritance is represented in the profile APIs by the optional property
`inherits_from` in request and response bodies. In profile `GET` requests, it
accompanies the relevant profile data for the space/room to indicate where this
data is coming from (either `global` or an ancestor space ID):
```
GET /_matrix/client/r0/profile/@me:example.org?scope=!space1:example.org
{
"inherits_from": "global",
"avatar_url": "mxc://example.org/myglobalavatar",
"displayname": "My global display name"
}
```
In `PUT` requests, `inherits_from` can be specified instead of
`avatar_url`/`displayname` to set which of a room's ancestor spaces (or the
global profile) to inherit from. Note that even though there are separate `PUT`
endpoints for `avatar_url` and `displayname`, setting `inherits_from` on either
of them will affect both, since inheritance applies to the entire profile.
```
PUT /_matrix/client/r0/profile/@me:example.org/avatar_url?scope=!space1:example.org
{
"inherits_from": "!space2:example.org"
}
```
On the other hand, if a `PUT` request is made without `inherits_from`, it turns
the space/room into a profile root, by copying whatever the previous profile was
and then updating the relevant `avatar_url`/`displayname` property.
Finally, if a profile `PUT` request does not specify `scope`, it behaves as
global profile updates currently do, except it only affects the `m.room.member`
state of rooms and spaces that inherit from `global`. This ensures that global
profile updates will not overwrite per-room/per-space profiles.
### Propagating inheritance
In order to give per-space profiles the desired semantics, whenever a space's
profile settings are updated, its children must be updated as well to inherit
from the right place. The following table outlines the transformations that may
occur for a space A, and how they affect its children:
||from `inherits_from` B|from profile root|
|-|-|-|
|to `inherits_from` C|All children of A that inherited from B recursively changed to inherit from C|All children of A that inherited from A recursively changed to inherit from C|
|to profile root|All children of A that inherited from B recursively changed to inherit from A|No change in inheritance|
### <a id="inheritance-restrictions"/>Inheritance restrictions
In addition, there are some restrictions on what a space/room may inherit from.
If a space/room A inherits from a space B, then B must be an ancestor of A
(determined by following `m.space.child` links), and there must be a direct path
in the space-child graph from B to A that only passes through children which the
user has joined and which are *not* profile roots. This is to prevent
undesirable situations such as a space A having a subspace B which has a child
room C, where B is a profile root and yet C somehow inherits from A.
### Automatic inheritance changes
Per-space profiles are only really useful if they automatically propagate to
newly joined/added rooms and subspaces. Also, servers generally need to keep
track of when rooms and subspaces are left/removed in order to ensure that the
above inheritance conditions are upheld. For these reasons, servers implementing
per-room/per-space profiles must apply the following rules:
- When the user joins a space/room, perform a breadth-first search for an ancestor space that is a profile root, by following `m.space.child` links backwards through spaces the user has joined. Break any ties by selecting the first in a lexicographic ordering of room IDs, and then set the joined space/room to inherit from this profile root, or the global profile if none is found. This profile data should go immediately into the initial `join` state, rather than being updated after the join is complete.
- When the user leaves a space A, if A was a profile root, reset every space/room that inherited from A to inherit from `global`. Otherwise if A inherited from a space B, reset every child of A that inherited from B to inherit from `global`.
- When the user adds a space/room A to a space B, if A inherited from `global` and B is a profile root, set A to inherit from B. Otherwise if A inherited from `global` and B inherits from a space C, set A to inherit from C.
- When a space/room A is removed from a space B (whether by the user or someone else), if A inherited from a space C, check whether A is still allowed to inherit from C by [the above rules](#inheritance-restrictions). If this is no longer the case, then reset A to inherit from `global`.
### Errors
Given that this proposal expands the surface of the profile APIs, there are some
new ways in which they can fail:
- The `scope` profile APIs are only for interacting with one's own profiles. Thus if a user attempts to set/get another user's profile for a given `scope`, the server must return a 403 with `M_FORBIDDEN`.
- Similarly, if a user attempts to set/get their profile for a `scope` which they have not joined / might not exist, the server must return a 403 with `M_FORBIDDEN`.
- If the user attempts to set `inherits_from` to an [invalid value](#inheritance-restrictions), the server must return a 400 with `M_UNKNOWN` along with a more specific explanation.
## Potential issues
There is a pre-existing issue with the profile APIs, namely that updating one's
profile is an O(n) operation with the number of rooms it affects, often taking
multiple minutes to complete on larger accounts. Arguably this should be solved
as part of this proposal by linking through to
[extensible profile rooms](https://github.com/matrix-org/matrix-doc/pull/1769)
in `m.room.member` state, which would allow the most common use-case of profile
updates to be O(1). While this is not undertaken here due to the significant
added complexity, this proposal is structured in a way to hopefully be
compatible with any future changes in this direction.
## Alternatives
An alternative would be to store all per-room/per-space profile data in a single
global [extensible profile](https://github.com/matrix-org/matrix-doc/pull/1769),
essentially keeping a public mapping of room IDs → profile data. However, this
alternative would leak data about users' profiles in private rooms, which is a
significant privacy concern, and it is unclear how conflicting profiles would
affect the "one source of truth" given by `m.room.member` state.
## Security considerations
None that I am aware of.
## Unstable prefix
During development of this feature the versions of the profile APIs augmented
with `scope` will be available at unstable endpoints:
```text
GET /_matrix/client/unstable/town.robin.msc3189/profile/{userId}
GET /_matrix/client/unstable/town.robin.msc3189/profile/{userId}/avatar_url
PUT /_matrix/client/unstable/town.robin.msc3189/profile/{userId}/avatar_url
GET /_matrix/client/unstable/town.robin.msc3189/profile/{userId}/displayname
PUT /_matrix/client/unstable/town.robin.msc3189/profile/{userId}/displayname
```
Loading…
Cancel
Save