-
Notifications
You must be signed in to change notification settings - Fork 392
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
MSC3189: Per-room/per-space profiles #3189
base: old_master
Are you sure you want to change the base?
Changes from all commits
4fd6f1a
7d01509
2479921
d1aa8d3
a77d7ae
96c8c03
fb6c4a2
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,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 | ||
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 unfortunate. Why can't I create any random room to be my "alter ego" and configure arbitrary rooms to inherit from it? 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. The intent of tying profile data to the space hierarchy is to avoid confusing users by introducing yet another perspective to organize one's rooms and spaces from. Also, I feel it would be weird to allow sibling rooms on otherwise equal footing to be able to inherit from each other - thus I believe that putting profile data in spaces is the most practical and user friendly option, given that users will generally already have rooms grouped together in a space when they want them to share profiles. 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. But I feel that this is leaving common use cases on the table. What if I have two profiles I use often and just want to pick one for each room? It seems weird that I can't do this if they don't align with spaces I am in.
I don't think there is anything preventing cycles in spaces so while it may be "weird" it isn't actually avoiding any complexity in the algorithm. I don't think this is enough of a reason to deny it. |
||
|
||
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. | ||
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. If I understand the algorithm correctly it may prefer my global profile over a custom profile because the parent that was lexicographically first didn't have a custom profile set. Does it make more sense to say "The custom profile of the nearest parent with a custom profile (ties broken lexicographically). Falling back to the global profile if no parent has a custom profile set). I don't think that "nearest parent" is a perfect choice but it seems reasonable. The important part is that I think we should always prefer a custom profile in any parent over the global. Furthermore while the auto-guess algorithm makes sense to have it should really be user choice. For example Element has a screen with some room info and a "Join" button. It would make sense to also have an indicator of what profile will be used and the option to select a different one. However one complication is that the client may not know which spaces a room is in until they join. 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.
Yes, this is essentially what I mean. The global profile is intended to be a last resort, only chosen if the server has traversed the room's entire ancestor graph and found no profile roots. (might see if I can reword this better)
I also think it would be great UX-wise to allow clients to choose profiles before joining. Clients should have enough information to be able to tell which spaces a room could inherit from just by looking at the |
||
- 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`. | ||
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 seems surprising! Maybe the custom profile should be "popped out" to its own room and references redirected. Furthermore this makes me wonder if a space is the right place to store a profile. What if I want to use the same custom profile in two spaces? Maybe I work for CompanyA and we partner with CompanyB so I want to use my same "Work" custom profile for both. I see the need to link the profiles to spaces, it is the more reasonable way to pick a default profile for new rooms, but I wonder if the profile data should actually live elsewhere. For example my profile is a room and it references which spaces it applies to. When changing that profile I change children of the referenced spaces. Then leaving a space can be updating the referenced spaces to remove the space itself and add the children (if desired). 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.
My assumption is that if a user wants to use the same profile across multiple spaces, they can either create a parent space ("Work") encompassing them both, or just maintain the two profiles independently. Neither option seems especially burdensome, especially in the case of two spaces. 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.
Hm, I'd feel weird about having a behavior where leaving a room causes you to spontaneously join a new one. 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 don't see why this is weird. I had a profile before. Now I still have a profile. Note that if as mentioned above these rooms aren't necessarily spaces they don't need to show up in my space list so it isn't weird to a user. |
||
- 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`. | ||
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 feels wrong to me. I would expect that the profile is sticky. I like the idea of using space membership to easily apply it to existing and future rooms but it seems that one applied it should stay until I explicitly change it. |
||
|
||
### 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. | ||
Comment on lines
+137
to
+142
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. Maybe both of these could work together. I think that using spaces to apply profiles makes a lot of sense, and I think that this MSC is in the right direction, just a couple of minor tweaks. However I also think that storing the profiles in their own room makes a lot of sense, this allows sharing a profile between spaces and makes the behaviour simpler if you leave or are kicked from a space. So maybe they could work together to make an awesome solution.
|
||
|
||
## Security considerations | ||
|
||
None that I am aware of. | ||
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.
Neither of these are new, but I think worth noting as they are unsolved and relevant. |
||
|
||
## 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 | ||
``` |
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.
Instead of a magic string "global" should we just use
null
to specify the global profile? This also has nice symmetry with omitting thescope
URL param.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.
Possibly, though I personally like having a self-explanatory keyword, and would like to avoid situations where
undefined
andnull
have not-so-subtle differences in meaning.