Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
632c83e
Add experimental feature flag `msc4429_enabled`
anoadragon453 Mar 13, 2026
d0b616d
Schema for new profile updates stream table
anoadragon453 Mar 13, 2026
63cfbe7
Add profile updates stream
anoadragon453 Mar 13, 2026
e45045d
Implement replication
anoadragon453 Mar 13, 2026
d9adfd8
Allow fetching latest changes via /sync and filtering
anoadragon453 Mar 13, 2026
30e8737
Add `msc4429_enabled` to Complement tests
anoadragon453 Mar 13, 2026
cfb7034
newsfile
anoadragon453 Mar 13, 2026
5b4d505
Update unit tests with new token format
anoadragon453 Mar 17, 2026
86572c6
Constrain profile endpoint handling to `profile_updates` writer workers
anoadragon453 Mar 17, 2026
3221271
Prevent 405 Method Not Allowed
anoadragon453 Mar 17, 2026
f27ca70
Correct the Complement worker endpoint configuration
anoadragon453 Mar 17, 2026
e37a200
Merge remote-tracking branch 'origin/develop' into anoa/msc4429
Half-Shot May 1, 2026
f95d28a
Bump schema for profile updates
Half-Shot May 1, 2026
8accba6
Merge branch 'develop' of github.com:element-hq/synapse into anoa/msc…
anoadragon453 May 26, 2026
343d4df
Drop `profile_updates_fk_users` foreign key
jaywink May 27, 2026
386958c
Add a timestamp column to `profile_updates`
jaywink May 27, 2026
c02e6a6
Only pass field names to `add_profile_updates`
jaywink May 27, 2026
4e820ca
Multiline string for `_get_updated_profile_updates_txn`
jaywink May 27, 2026
7e964a9
`user_id: UserID` in `ProfileUpdatesStreamRow`
jaywink May 27, 2026
fcbceee
Don't return anything for users without profiles in `_generate_initia…
jaywink May 27, 2026
2fa20e0
Only support unstable prefix for `profile_fields` in API filters
jaywink May 27, 2026
6ec8ee2
Mount ProfileFieldRestServlet on all Synapse instances
jaywink May 28, 2026
df1b587
Add `ReplicationProfileSetFieldValue` endpoint
jaywink May 28, 2026
e4c35ce
Add missing value to docstring
jaywink May 28, 2026
c30e0d9
Also replicate DELETE profile fields to the right stream worker
jaywink May 28, 2026
352efe1
Fix replication of profile updates to the right stream writer
jaywink May 29, 2026
56a3647
Remove unnecessary upgrade notes for profile updates stream writer
jaywink May 29, 2026
632c7ea
Add a docstring, remove premature optimization
jaywink May 29, 2026
92bfed7
Don't return null's in initial sync for profiles with no value for th…
jaywink May 29, 2026
4697e73
Simplify collecting profile field updates for sync
jaywink May 29, 2026
3444aef
Improve docstrings of `get_updated_profile_updates` and `get_profile_…
jaywink Jun 1, 2026
4c2bbbc
Use a "many" insert for `add_profile_updates`
jaywink Jun 1, 2026
4cf5e09
Clarify `_generate_initial_sync_entry_for_profile_updates` is for loc…
jaywink Jun 1, 2026
c082da4
Allow a `msc4429_enabled` config alias outside of the experimental co…
jaywink Jun 1, 2026
aeeb933
Restrict initial sync profiles to local users for now
jaywink Jun 1, 2026
54c299d
Clarify newsfile that this pr is for local users only
jaywink Jun 1, 2026
3cf7555
Clarify profile updates sync response with a comment
jaywink Jun 1, 2026
d254167
Clarify in `_generate_sync_entry_for_profile_updates` that only local…
jaywink Jun 1, 2026
391fd2b
Merge branch 'develop' into anoa/msc4429
jaywink Jun 1, 2026
e236ddb
Add SyncProfileUpdatesTestCase
jaywink Jun 2, 2026
1ae91c8
Fix `get_profile_data_for_users` if user does not have custom fields
jaywink Jun 2, 2026
1a59c0a
Only return profiles for initial sync that have events when using laz…
jaywink Jun 2, 2026
bd40add
Add some tests for profile handler updating profile_updates stream ta…
jaywink Jun 3, 2026
b61f748
Fix some broken tests due to sync token changes
jaywink Jun 3, 2026
36f0f64
Add "action" to profile updates stream
jaywink Jun 4, 2026
301e9c1
ProfileUpdate.action should never be None
jaywink Jun 4, 2026
2d9f943
Collect lazy loaded members for profile updates in sync response from…
jaywink Jun 4, 2026
e6c25e4
Add `profile_updates_sequence` sequence to synapse_port_db.py
jaywink Jun 4, 2026
28587d7
Fix the lazy loading test cases
jaywink Jun 5, 2026
d02150f
Remove code that accidentally came here from https://github.com/eleme…
jaywink Jun 5, 2026
877074f
Lint tests/handlers/test_sync.py
jaywink Jun 5, 2026
e90096e
Use a "per user profile updates" tracking table
jaywink Jun 5, 2026
21d08a0
Optimize calculating left room id's for incremental sync
jaywink Jun 5, 2026
029c4d8
Create `profile_updates_per_user`
anoadragon453 Jun 21, 2026
676ef4f
Read from `profile_updates_per_user` when syncing
anoadragon453 Jun 21, 2026
4a3fc09
fix the tests
anoadragon453 Jun 21, 2026
ab9c917
Remove `test_incremental_sync_sends_down_null_profile_if_user_no_long…
anoadragon453 Jun 21, 2026
874dc35
Merge remote-tracking branch 'origin/develop' into anoa/msc4429
jaywink Jun 22, 2026
27417cf
Remove mypy ignore
jaywink Jun 22, 2026
daa8d7a
Ensure lazy loaded profile updates in sync contain full profiles for …
jaywink Jun 23, 2026
5819dcc
Use a cache to ensure we don't re-send the full profile fields of use…
jaywink Jun 23, 2026
09e2125
Fix incremental lazy sync to send down profiles for users without pro…
jaywink Jun 23, 2026
339eee5
WIP track when a user leaves a room in `profile_updates_per_user`
anoadragon453 Jun 22, 2026
2dbaec3
Send down a null when user has left all shared rooms
jaywink Jun 23, 2026
8504386
Send down full profile of user if user has joined during incremental …
jaywink Jun 23, 2026
50e788b
Add foreign key to `profile_updates_per_user.stream_id`
jaywink Jun 23, 2026
ddb0c52
Actually run MSC4429 complement tests in CI
anoadragon453 Jun 23, 2026
9d58d19
Merge remote-tracking branch 'origin/develop' into anoa/msc4429
jaywink Jun 24, 2026
d3aacee
Fix `test_update_profile_set_field_writes_to_per_user_profile_trackin…
jaywink Jun 24, 2026
7c728ec
Optimize profile handler `user_joined_room` and `user_left_room`
jaywink Jun 24, 2026
05634f9
Ensure `room_member` handler writes profile updates to the right prof…
jaywink Jun 24, 2026
b657d2f
Add a prune job for the profile updates stream tables
jaywink Jun 24, 2026
ea12b22
Merge remote-tracking branch 'origin/develop' into anoa/msc4429
jaywink Jun 24, 2026
3e4301a
Remove unnecessary if
jaywink Jun 25, 2026
5f761ab
Use set for uniqueness
jaywink Jun 25, 2026
bf10187
Docstring for ProfileUpdateAction
jaywink Jun 25, 2026
585590b
Protect against stream being rewound
jaywink Jun 25, 2026
e278dfd
Restore initial sync behaviour to return full profiles of users curre…
jaywink Jun 25, 2026
8bace9f
Fix some types in various places regarding profile field values
jaywink Jun 25, 2026
7a60657
Clean up recording profile updates on changes
jaywink Jun 26, 2026
f453e03
Remove more useless casts
jaywink Jun 26, 2026
b2246de
Ensure we don't call `_dispatch_record_profile_updates` with an empty…
jaywink Jun 26, 2026
b459512
Use `include_profile_updates_in_sync` config item instead of an exper…
jaywink Jun 26, 2026
4a874c4
Remove invalid docs on `profile_updates` stream
jaywink Jun 26, 2026
088f230
Gate more things behind msc4429 being enabled
jaywink Jun 26, 2026
c12623e
Lint server config
jaywink Jun 26, 2026
1fc8d4c
Move profile updates SQL to latest delta
jaywink Jun 29, 2026
b0950f9
Merge remote-tracking branch 'origin/develop' into anoa/msc4429
jaywink Jun 29, 2026
bf3e133
Fix `SCHEMA_VERSION` in __init__.py
jaywink Jun 29, 2026
60ed175
When user has left all rooms shared with a user, clear any previous p…
jaywink Jun 29, 2026
2377165
Add test to prove initial sync doesn't include users updates who don'…
jaywink Jun 29, 2026
496728d
Add docstrings to tests
jaywink Jun 29, 2026
5064349
Don't delete custom profile field when setting an empty string
jaywink Jun 29, 2026
a460c28
Merge remote-tracking branch 'origin/develop' into anoa/msc4429
jaywink Jun 29, 2026
eb5aa21
Ensure both initial and incremental syncs include our own profile and…
jaywink Jul 1, 2026
e6aec55
Merge remote-tracking branch 'origin/develop' into anoa/msc4429
jaywink Jul 1, 2026
ecd890a
Fix MSC4429 referring comment
jaywink Jul 1, 2026
6a5adb9
Remove extra `ids` definition
jaywink Jul 1, 2026
45e9a01
Don't unpact `ProfileUpdateAction` unnecessarily early
jaywink Jul 1, 2026
6730ad0
Add `str` to ProfileUpdateAction types
jaywink Jul 1, 2026
28563f0
Make `ProfileUpdateAction` docstrings more descriptive on what the di…
jaywink Jul 1, 2026
f6c1db7
Fix `StreamKeyType.__str__`
jaywink Jul 1, 2026
180c6a2
tuples in `_track_profile_updates_per_user_txn`
jaywink Jul 1, 2026
e74046a
Remove confusing wording
jaywink Jul 1, 2026
4271ffa
Add primary key to `profile_updates_per_user`
jaywink Jul 1, 2026
4934fca
Clean up some SQL for easier reading
jaywink Jul 1, 2026
c7f3b79
Use `deleted_stream_id` instead of indexing on `row`
jaywink Jul 1, 2026
e68560a
Don't dump to json string in tests
jaywink Jul 2, 2026
8685137
Correctly type `ProfileUpdatesStreamRow.action`
jaywink Jul 2, 2026
56b7de2
Correctly type `ProfileUpdatesStreamRow.field_name`
jaywink Jul 2, 2026
182e4c2
Clarify variable names and add comments to `ReplicationDataHandler`
jaywink Jul 2, 2026
2c7b418
Ensure we filter out federated users in lazy loading sync when collec…
jaywink Jul 2, 2026
6f6e633
Ensure we don't respond with profile fields the client didn't ask for
jaywink Jul 3, 2026
d3dd12f
Rewrite `lazy_loaded_profile_fields_cache`
jaywink Jul 3, 2026
1955fbe
Ensure falsey values survive the set_field / sync profile updates -cycle
jaywink Jul 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.d/19556.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add optional support for [MSC4429: Profile Updates for Legacy Sync](https://github.com/matrix-org/matrix-spec-proposals/pull/4429).
Currently defaults to not enabled, and is limited to local users only for the sync results.
3 changes: 2 additions & 1 deletion docker/complement/conf/start_for_complement.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,13 @@ if [[ -n "$SYNAPSE_COMPLEMENT_USE_WORKERS" ]]; then
federation_inbound, \
federation_reader, \
federation_sender, \
profile_updates, \
synchrotron, \
client_reader, \
appservice, \
pusher, \
device_lists:2, \
stream_writers=account_data+presence+receipts+to_device+typing"
stream_writers=account_data+presence+profile_updates+receipts+to_device+typing"

fi
log "Workers requested: $SYNAPSE_WORKER_TYPES"
Expand Down
2 changes: 2 additions & 0 deletions docker/complement/conf/workers-shared-extra.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ enable_registration_without_verification: true
bcrypt_rounds: 4
url_preview_enabled: true
url_preview_ip_range_blacklist: []
# MSC4429 Profile updates down legacy /sync
include_profile_updates_in_sync: true

## Registration ##

Expand Down
15 changes: 14 additions & 1 deletion docker/configure_workers_and_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,10 @@
"^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/(join|invite|leave|ban|unban|kick)$",
"^/_matrix/client/(api/v1|r0|v3|unstable)/join/",
"^/_matrix/client/(api/v1|r0|v3|unstable)/knock/",
"^/_matrix/client/(api/v1|r0|v3|unstable)/profile/",
# The [^/] differentiates this endpoint from
# `ProfileRestFieldsServlet`, which we want to instead go to the
# `profile_updates` worker below.
"^/_matrix/client/(api/v1|r0|v3|unstable)/profile/[^/]+",
],
"shared_extra_conf": {},
"worker_extra_conf": "",
Expand Down Expand Up @@ -308,6 +311,15 @@
"shared_extra_conf": {},
"worker_extra_conf": "",
},
"profile_updates": {
"app": "synapse.app.generic_worker",
"listener_resources": ["client", "replication"],
"endpoint_patterns": [
"^/_matrix/client/(unstable/uk.tcpip.msc4133|api/v1|r0|v3|unstable)/profile/.+/"
],
"shared_extra_conf": {},
"worker_extra_conf": "",
},
"device_lists": {
"app": "synapse.app.generic_worker",
"listener_resources": ["client", "replication"],
Expand Down Expand Up @@ -517,6 +529,7 @@ def add_worker_roles_to_shared_config(
"typing",
"push_rules",
"thread_subscriptions",
"profile_updates",
}

# Worker-type specific sharding config. Now a single worker can fulfill multiple
Expand Down
2 changes: 2 additions & 0 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4486,6 +4486,8 @@ This setting has the following sub-options:

* `device_lists` (string): Name of a worker assigned to the `device_lists` stream.

* `profile_updates` (string): Name of a worker assigned to the `profile_updates` stream.

Example configuration:
```yaml
stream_writers:
Expand Down
1 change: 1 addition & 0 deletions docs/workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ configured as stream writer for the `quarantined_media_changes` stream:

^/_synapse/admin/v1/quarantine_media/.*$


#### Restrict outbound federation traffic to a specific set of workers

The
Expand Down
3 changes: 3 additions & 0 deletions schema/synapse-config.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5541,6 +5541,9 @@ properties:
device_lists:
type: string
description: Name of a worker assigned to the `device_lists` stream.
profile_updates:
type: string
description: Name of a worker assigned to the `profile_updates` stream.
default: {}
examples:
- events: worker1
Expand Down
1 change: 1 addition & 0 deletions scripts-dev/complement.sh
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ main() {
./tests/msc4155
./tests/msc4306
./tests/msc4222
./tests/msc4429
)

# Export the list of test packages as a space-separated environment variable, so other
Expand Down
4 changes: 4 additions & 0 deletions synapse/_scripts/synapse_port_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,10 @@ def alter_table(txn: LoggingTransaction) -> None:
"quarantined_media_id_seq",
[("quarantined_media_changes", "stream_id")],
)
await self._setup_sequence(
"profile_updates_sequence",
[("profile_updates", "stream_id")],
)

# Step 3. Get tables.
self.progress.set_state("Fetching tables")
Expand Down
50 changes: 50 additions & 0 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,56 @@ class ProfileFields:
AVATAR_URL: Final = "avatar_url"


class ProfileUpdateAction(str, enum.Enum):
"""
Enum representing the action of a row in the profile updates stream tables.
These actions are used to determine how profiles, and what data is included in the
sync responses, depending on field updates and room membership changes.
"""

JOINED_ROOM = "joined_room"
"""
This profile update row action represents a user joining a room.

When gathering an incremental sync non-lazy response for profile updates,
we always include the full profile of users who have joined a room the syncing
user is a member of, where full profile means all the current profile values the
client asked for, regardless of whether they have changed recently. This ensures
that clients have profile re-populated for any users who have recently left
shared rooms.

A scenario example would be as follows:

* Alice leaves a room with Bob
* Bob's client clears all profile fields from Alice
* Alice joins a room with Bob
* Bob's client does an incremental non-lazy sync

At the end of the flow Bob should receive all the profile fields the client
is interested in, not just the potential diff, which non-lazy incremental sync
normally includes. This update action currently has no meaning for sync responses
that are not incremental and non-lazy.
"""
LEFT_ROOM = "left_room"
"""
This profile update row action represents a user leaving a room.

Clients will want to know when they no longer share rooms with a user. This
profile action row allows the sync code to deliver a `null` response for those
profiles, so clients can clear their cache containing the users profile data
they are no longer interested in.
"""
UPDATE = "update"
"""
This profile update row action represents a user updating a profile field.

Depending on the type of sync (initial/incremental, lazy/non-lazy), either the
diff of profile field updates or all the current profile fields are included
in the sync response. In the latter case the profile update action row signifies
a change, but the client may still get fields that have not changed.
"""


class StickyEventField(TypedDict):
"""
Dict content of the `sticky` part of an event.
Expand Down
17 changes: 17 additions & 0 deletions synapse/api/filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,23 @@
"filter": FILTER_SCHEMA,
"room_filter": ROOM_FILTER_SCHEMA,
"room_event_filter": ROOM_EVENT_FILTER_SCHEMA,
"profile_fields_filter": {
"type": "object",
"properties": {
"ids": {"type": "array", "items": {"type": "string"}},
},
"additionalProperties": True,
},
},
"properties": {
"presence": {"$ref": "#/definitions/filter"},
"account_data": {"$ref": "#/definitions/filter"},
"room": {"$ref": "#/definitions/room_filter"},
"event_format": {"type": "string", "enum": ["client", "federation"]},
"event_fields": {"type": "array", "items": {"type": "string"}},
"org.matrix.msc4429.profile_fields": {
"$ref": "#/definitions/profile_fields_filter"
},
},
"additionalProperties": True, # Allow new fields for forward compatibility
}
Expand Down Expand Up @@ -217,6 +227,13 @@ def __init__(self, hs: "HomeServer", filter_json: JsonMapping):
self.event_fields = filter_json.get("event_fields", [])
self.event_format = filter_json.get("event_format", "client")

self.profile_fields: set[str] = set()
if hs.config.server.include_profile_updates_in_sync:
profile_fields_filter = filter_json.get("org.matrix.msc4429.profile_fields")

if isinstance(profile_fields_filter, Mapping):
self.profile_fields = set(profile_fields_filter.get("ids", []))

def __repr__(self) -> str:
return "<FilterCollection %s>" % (json.dumps(self._filter_json),)

Expand Down
6 changes: 6 additions & 0 deletions synapse/config/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,12 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
" 'allow_public_rooms_over_federation' is set."
)

# Whether to support MSC4299 profile updates down legacy /sync
self.include_profile_updates_in_sync = config.get(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(should this be in the config manual & JSONSchema? I'm not sure either way)

"include_profile_updates_in_sync",
Comment on lines +548 to +550

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Whether to support MSC4299 profile updates down legacy /sync
self.include_profile_updates_in_sync = config.get(
"include_profile_updates_in_sync",
# Whether to support MSC4429 profile updates down legacy /sync
self.include_profile_updates_in_sync = config.get(
"include_profile_updates_in_sync",

Separately, should this flag include the name MSC4429 in it?

Once the feature is stabilised, I am not sure if we would want to keep this option permanently ­— I suspect not as we would just enable the stable version out of the box — but the name of the feature flag doesn't make it obvious that this is the case.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it depends if we want to use this same option for MSC4262, ie sliding sync profile updates support? I was planning on using the same configuration option for both, since they're pretty much the same thing but for a different sync engine. If that sounds ok, I'll leave without the msc in the option, if not, we can have two config options.

False,
)

# Check if the legacy "restrict_public_rooms_to_local_users" flag is set. This
# flag is now obsolete but we need to check it for backward-compatibility.
if config.get("restrict_public_rooms_to_local_users", False):
Expand Down
25 changes: 22 additions & 3 deletions synapse/config/workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ class WriterLocations:
push_rules: The instances that write to the push stream. Currently
can only be a single instance.
device_lists: The instances that write to the device list stream.
thread_subscriptions: The instances that write to the thread subscriptions
stream.
profile_updates: The instances that write to the profile updates stream.
quarantined_media_changes: The instances that write to the quarantined media
changes stream.
"""
Expand Down Expand Up @@ -179,7 +182,11 @@ class WriterLocations:
converter=_instance_to_list_converter,
)
thread_subscriptions: list[str] = attr.ib(
default=["master"],
default=[MAIN_PROCESS_INSTANCE_NAME],
converter=_instance_to_list_converter,
)
profile_updates: list[str] = attr.ib(
default=[MAIN_PROCESS_INSTANCE_NAME],
converter=_instance_to_list_converter,
)
quarantined_media_changes: list[str] = attr.ib(
Expand Down Expand Up @@ -361,8 +368,7 @@ def read_config(
writers = config.get("stream_writers") or {}
self.writers = WriterLocations(**writers)

# Check that the configured writers for events and typing also appears in
# `instance_map`.
# Check that the configured writers also appear in `instance_map`.
for stream in (
"events",
"typing",
Expand All @@ -371,6 +377,9 @@ def read_config(
"receipts",
"presence",
"push_rules",
"device_lists",
"thread_subscriptions",
"profile_updates",
):
instances = _instance_to_list_converter(getattr(self.writers, stream))
for instance in instances:
Expand Down Expand Up @@ -421,6 +430,16 @@ def read_config(
"Must specify at least one instance to handle `device_lists` messages."
)

if len(self.writers.thread_subscriptions) == 0:
raise ConfigError(
"Must specify at least one instance to handle `thread_subscriptions` messages."
)

if len(self.writers.profile_updates) == 0:
raise ConfigError(
"Must specify at least one instance to handle `profile_updates` messages."
)

self.events_shard_config = RoutableShardedWorkerHandlingConfig(
self.writers.events
)
Expand Down
Loading
Loading