MSC4429: Profile Updates for Legacy Sync - #4429
anoadragon453 wants to merge 7 commits into
Conversation
82ac3a7 to
3b205f6
Compare
There was a problem hiding this comment.
Implementation requirements:
- Client (using)
- Server (sending)
There was a problem hiding this comment.
Introduces the `matrix_synapse_experimental_features_msc4429_enabled` variable (disabled by default), allowing Synapse to notify clients using the legacy /sync endpoint of profile changes for other users. See <matrix-org/matrix-spec-proposals#4429> Signed-off-by: Norman Ziegner <n.ziegner@hzdr.de>
To indicate to clients that they can stop tracking a given user's profile.
There was a problem hiding this comment.
Pull request overview
Adds a new MSC proposing how to deliver custom profile field updates over legacy /sync (similar in spirit to MSC4262 for Sliding Sync), including opt-in filtering and guidance for initial population/lazy-loading.
Changes:
- Proposes a new top-level
usersobject in/synccarrying per-userprofile_updates. - Introduces a new filter field (
profile_fields.ids) to opt-in to specific profile field IDs. - Documents expected server/client behavior (initial sync behavior, lazy-loading interactions, implementation notes).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Each entry in `profile_updates` represents a profile field and any changes to | ||
| its value. If its value is `null`, the field is treated as having been removed | ||
| from the user's profile (rather than literally being set to `null`). |
There was a problem hiding this comment.
@zecakeh pointed out on the equivalent Sliding Sync MSC that:
From the definition of
PUT /profile/{userId}/{keyName}:Servers MAY reject
nullvalues. Servers that acceptnullvalues SHOULD store them rather than treatingnullas a deletion request. Clients that want to delete a field, including its key and value, SHOULD use theDELETEendpoint instead.
So actually null is a valid value, and we can't use it as a signal to clients that a field has been deleted, unfortunately.
Instead, we'll probably want a removed_profile_fields array or similar:
{
"users": {
"@user:example.org": {
"profile_updates": {
"m.status": {
"text": "Swimming in the Great Lakes!",
"emoji": "🏊️"
},
},
"removed_profile_fields": ["m.other_field"]
}
}
}There was a problem hiding this comment.
This sounds like it is re-treading conversations in #3391
There was a problem hiding this comment.
This sounds like it is re-treading conversations in #3391
I'm not sure, in this case there's already an established mechanism to delete a profile field (DELETE), that was about whether to add one.
What about either defining undefined as a deletion or else just changing it in this MSC so null is not an acceptable value? Having to add a separate field to track removed fields seems just unnecessary.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| { | ||
| "users": { | ||
| "@user:example.org": { | ||
| "profile_updates": { |
There was a problem hiding this comment.
It would be very beneficial if the server could flag the difference between a profile delta and a complete profile to replace whatever the client has in cache.
Without that distinction, the server is limited in how much clean-up it can do; it has to (at the very least) remember the whole set of 'removed profile fields' for all of time.
Why? Because we never know what state the client is in. If we want to clean up our deltas server-side, we will have to, at some point, tell clients 'you are too old to get just the delta, here's the whole profile instead'.
Since we can only currently speak in deltas, then sending a 'whole profile' involves tracking all field names that we might have sent to a client in the past.
Maybe the keys we reserve are something like:
profile: (fields)profile_reset:true(marks this as a 'whole profile', not a delta. Should this be automatically implied for an initial sync?)profile_remove: list of removed field names
You could, I suppose, make profile_remove: true the 'whole profile' marker. I don't know if that kind of double-duty is a good or bad thing. profile_reset and profile_remove do in fact clash so this 'naturally' resolves that clash by making it impossible
| The field name `users` fits in alongside `rooms`. Using a nested | ||
| `profile_updates` dictionary allows extending the dictionary beneath each user | ||
| ID in the future if desired. | ||
|
|
There was a problem hiding this comment.
It is what it is, but profile_updates feels like a chunky key when it could have been called profile. It will be repeated a lot of times in the response body potentially.
|
|
||
| ### Clients | ||
|
|
||
| Clients are recommended to enable lazy-loading in their `/sync` requests to |
There was a problem hiding this comment.
Should include_redundant_members semantics apply also to profiles?
There was a problem hiding this comment.
Could you clarify what you mean here? Profile fields are sent when the profile updates, not with every event like member events?
There was a problem hiding this comment.
As far as I understand it, include_redundant_members means that when using lazy-loading sync, you get sent those user's membership events every time they appear in the timeline, not just the first time they appear in the timeline.
This is useful for clients that don't maintain a persistent cache of users, so they can always display the relevant information in-context without having to make CS API requests first.
The question is whether this exact same system should apply also to the profiles sent down when lazy-loading. You could imagine the same argument applying: clients might want to show the 'away' state next to the user's name and may not maintain a cache. The client might have been 're-launched' since witnessing the first timeline event from a given user, which is how they would have received their profile fields.
| of a given user; such as when a user no longer shares any room with another | ||
| user. For example: |
There was a problem hiding this comment.
I am partly wondering if this is actually necessary; would clients know to delete their profile cache because they already saw the user leave on their own?
There was a problem hiding this comment.
Clients will not always know if they no longer share a room with a user, for instance if the room was lazy-loaded.
Implements support for [MSC4429: Profile Updates for Legacy Sync](matrix-org/matrix-spec-proposals#4429). Paired with matrix-org/complement#849 and https://github.com/matrix-org/sytest/tree/anoa/msc4429 Tracking issue for removing unstable identifiers in Synapse: #19891 Further improvements tracked in: #19981 --------- Co-authored-by: Half-Shot <will@half-shot.uk> Co-authored-by: Jason Robinson <jasonr@element.io> Co-authored-by: Olivier 'reivilibre' <oliverw@element.io>
Implements support for [MSC4429: Profile Updates for Legacy Sync](matrix-org/matrix-spec-proposals#4429). Paired with matrix-org/complement#849 and https://github.com/matrix-org/sytest/tree/anoa/msc4429 Tracking issue for removing unstable identifiers in Synapse: element-hq/synapse#19891 Further improvements tracked in: element-hq/synapse#19981 --------- Co-authored-by: Half-Shot <will@half-shot.uk> Co-authored-by: Jason Robinson <jasonr@element.io> Co-authored-by: Olivier 'reivilibre' <oliverw@element.io>
|
|
||
| None identified. | ||
|
|
||
| ## Alternatives |
There was a problem hiding this comment.
More general: Why not put them into the member events like already done with displayname and avatar? Now we have two places to gather profile information.
There was a problem hiding this comment.
Profile fields are global and not based on per-room member events so I don't believe tacking them onto existing member events would make sense.
There was a problem hiding this comment.
But who decides what is global? Shouldn't we consider to remove these information from the member event to have one single source of truth?
There was a problem hiding this comment.
Putting them in per-room member events means we have the same ambiguity problems about what is intended to be global and what is intended to be per-room (e.g. per-room nicknames), so you don't know what you can and can't put in the user directory (for example) or use as a response to /profile.
The current profiles system (which is global) makes a lot of sense for this reason.
At the same time, removing the displayname and avatar from member events means that if the user's homeserver goes down, a new user joining the room won't know their name (or avatar). So there is some value in having these baked into the room state, even though the state bloat is undesirable.
(Either way, they can't be removed from room state yet for backwards compatibility reasons)
Due to MSC4429 being in flux and clients having implemented field removals early, the previously merged implementation pushed field removals by setting the value to `null`. The MSC will require delivering the fields in a separate `removed_profile_fields` key as `None` is a valid field value, see matrix-org/matrix-spec-proposals#4429 (comment) Thus, as we moved storing field deletes to `ProfileUpdateAction.DELETE`, we need to adjust the legacy sync to properly identify deletes. At the same time have implemented the new `removed_profile_fields` key, so we have it ready for simplifying the legacy sync response in the future.
Tested on NetBSD 10 amd64 with 2026Q2 environment. # Synapse 1.160.0 (2026-09-02) ## Features - Add experimental support for [MSC4502](matrix-org/matrix-spec-proposals#4502): Targeted and unrestricted room member queries. ([\#19974](element-hq/synapse#19974)) - Add optional support for [MSC4262: Profile Updates for Sliding Sync](matrix-org/matrix-spec-proposals#4262). Currently defaults to disabled, and is limited to local users only for the sync results. ([\#20003](element-hq/synapse#20003)) - Allow specifying multiple `action_name` and `status` query parameters when listing scheduled tasks via the admin API. ([\#20067](element-hq/synapse#20067)) # Synapse 1.159.0 (2026-08-18) ## Features - Add optional support for [MSC4429: Profile Updates for Legacy Sync](matrix-org/matrix-spec-proposals#4429). Currently defaults to not enabled, and is limited to local users only for the sync results. ([\#19556](element-hq/synapse#19556))
Rendered
Conflict of Interest declaration: I am employed by Element. This MSC was written as part of my work on the Element Backend Team to land a "user status" feature (based on top of custom profiles, see MSC4426) for a customer.
Implementations:
include_profile_updates_in_syncoption)