Make per-room/per-space profiles more first-class
This completely reworks MSC3189. Signed-off-by: Robin Townsend <robin@robin.town>pull/3189/head
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…
Reference in New Issue