Skip to content

Commit 729026e

Browse files
Sliding Sync: Add Account Data extension (MSC3959) (#17477)
Extensions based on [MSC3575](matrix-org/matrix-spec-proposals#3575): Sliding Sync
1 parent bdf37ad commit 729026e

File tree

6 files changed

+982
-2
lines changed

6 files changed

+982
-2
lines changed

changelog.d/17477.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add Account Data extension support to experimental [MSC3575](https://github.com/matrix-org/matrix-spec-proposals/pull/3575) Sliding Sync `/sync` endpoint.

synapse/handlers/sliding_sync.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from synapse.types import (
4747
DeviceListUpdates,
4848
JsonDict,
49+
JsonMapping,
4950
PersistedEventPosition,
5051
Requester,
5152
RoomStreamToken,
@@ -357,6 +358,7 @@ def __init__(self, hs: "HomeServer"):
357358
self.event_sources = hs.get_event_sources()
358359
self.relations_handler = hs.get_relations_handler()
359360
self.device_handler = hs.get_device_handler()
361+
self.push_rules_handler = hs.get_push_rules_handler()
360362
self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync
361363

362364
async def wait_for_sync_for_user(
@@ -628,6 +630,7 @@ async def handle_room(room_id: str) -> None:
628630

629631
extensions = await self.get_extensions_response(
630632
sync_config=sync_config,
633+
lists=lists,
631634
from_token=from_token,
632635
to_token=to_token,
633636
)
@@ -1797,13 +1800,15 @@ async def get_room_sync_data(
17971800
async def get_extensions_response(
17981801
self,
17991802
sync_config: SlidingSyncConfig,
1803+
lists: Dict[str, SlidingSyncResult.SlidingWindowList],
18001804
to_token: StreamToken,
18011805
from_token: Optional[SlidingSyncStreamToken],
18021806
) -> SlidingSyncResult.Extensions:
18031807
"""Handle extension requests.
18041808
18051809
Args:
18061810
sync_config: Sync configuration
1811+
lists: Sliding window API. A map of list key to list results.
18071812
to_token: The point in the stream to sync up to.
18081813
from_token: The point in the stream to sync from.
18091814
"""
@@ -1828,9 +1833,20 @@ async def get_extensions_response(
18281833
from_token=from_token,
18291834
)
18301835

1836+
account_data_response = None
1837+
if sync_config.extensions.account_data is not None:
1838+
account_data_response = await self.get_account_data_extension_response(
1839+
sync_config=sync_config,
1840+
lists=lists,
1841+
account_data_request=sync_config.extensions.account_data,
1842+
to_token=to_token,
1843+
from_token=from_token,
1844+
)
1845+
18311846
return SlidingSyncResult.Extensions(
18321847
to_device=to_device_response,
18331848
e2ee=e2ee_response,
1849+
account_data=account_data_response,
18341850
)
18351851

18361852
async def get_to_device_extension_response(
@@ -1956,3 +1972,125 @@ async def get_e2ee_extension_response(
19561972
device_one_time_keys_count=device_one_time_keys_count,
19571973
device_unused_fallback_key_types=device_unused_fallback_key_types,
19581974
)
1975+
1976+
async def get_account_data_extension_response(
1977+
self,
1978+
sync_config: SlidingSyncConfig,
1979+
lists: Dict[str, SlidingSyncResult.SlidingWindowList],
1980+
account_data_request: SlidingSyncConfig.Extensions.AccountDataExtension,
1981+
to_token: StreamToken,
1982+
from_token: Optional[SlidingSyncStreamToken],
1983+
) -> Optional[SlidingSyncResult.Extensions.AccountDataExtension]:
1984+
"""Handle Account Data extension (MSC3959)
1985+
1986+
Args:
1987+
sync_config: Sync configuration
1988+
lists: Sliding window API. A map of list key to list results.
1989+
account_data_request: The account_data extension from the request
1990+
to_token: The point in the stream to sync up to.
1991+
from_token: The point in the stream to sync from.
1992+
"""
1993+
user_id = sync_config.user.to_string()
1994+
1995+
# Skip if the extension is not enabled
1996+
if not account_data_request.enabled:
1997+
return None
1998+
1999+
global_account_data_map: Mapping[str, JsonMapping] = {}
2000+
if from_token is not None:
2001+
global_account_data_map = (
2002+
await self.store.get_updated_global_account_data_for_user(
2003+
user_id, from_token.stream_token.account_data_key
2004+
)
2005+
)
2006+
2007+
have_push_rules_changed = await self.store.have_push_rules_changed_for_user(
2008+
user_id, from_token.stream_token.push_rules_key
2009+
)
2010+
if have_push_rules_changed:
2011+
global_account_data_map = dict(global_account_data_map)
2012+
global_account_data_map[AccountDataTypes.PUSH_RULES] = (
2013+
await self.push_rules_handler.push_rules_for_user(sync_config.user)
2014+
)
2015+
else:
2016+
all_global_account_data = await self.store.get_global_account_data_for_user(
2017+
user_id
2018+
)
2019+
2020+
global_account_data_map = dict(all_global_account_data)
2021+
global_account_data_map[AccountDataTypes.PUSH_RULES] = (
2022+
await self.push_rules_handler.push_rules_for_user(sync_config.user)
2023+
)
2024+
2025+
# We only want to include account data for rooms that are already in the sliding
2026+
# sync response AND that were requested in the account data request.
2027+
relevant_room_ids: Set[str] = set()
2028+
2029+
# See what rooms from the room subscriptions we should get account data for
2030+
if (
2031+
account_data_request.rooms is not None
2032+
and sync_config.room_subscriptions is not None
2033+
):
2034+
actual_room_ids = sync_config.room_subscriptions.keys()
2035+
2036+
for room_id in account_data_request.rooms:
2037+
# A wildcard means we process all rooms from the room subscriptions
2038+
if room_id == "*":
2039+
relevant_room_ids.update(sync_config.room_subscriptions.keys())
2040+
break
2041+
2042+
if room_id in actual_room_ids:
2043+
relevant_room_ids.add(room_id)
2044+
2045+
# See what rooms from the sliding window lists we should get account data for
2046+
if account_data_request.lists is not None:
2047+
for list_key in account_data_request.lists:
2048+
# Just some typing because we share the variable name in multiple places
2049+
actual_list: Optional[SlidingSyncResult.SlidingWindowList] = None
2050+
2051+
# A wildcard means we process rooms from all lists
2052+
if list_key == "*":
2053+
for actual_list in lists.values():
2054+
# We only expect a single SYNC operation for any list
2055+
assert len(actual_list.ops) == 1
2056+
sync_op = actual_list.ops[0]
2057+
assert sync_op.op == OperationType.SYNC
2058+
2059+
relevant_room_ids.update(sync_op.room_ids)
2060+
2061+
break
2062+
2063+
actual_list = lists.get(list_key)
2064+
if actual_list is not None:
2065+
# We only expect a single SYNC operation for any list
2066+
assert len(actual_list.ops) == 1
2067+
sync_op = actual_list.ops[0]
2068+
assert sync_op.op == OperationType.SYNC
2069+
2070+
relevant_room_ids.update(sync_op.room_ids)
2071+
2072+
# Fetch room account data
2073+
account_data_by_room_map: Mapping[str, Mapping[str, JsonMapping]] = {}
2074+
if len(relevant_room_ids) > 0:
2075+
if from_token is not None:
2076+
account_data_by_room_map = (
2077+
await self.store.get_updated_room_account_data_for_user(
2078+
user_id, from_token.stream_token.account_data_key
2079+
)
2080+
)
2081+
else:
2082+
account_data_by_room_map = (
2083+
await self.store.get_room_account_data_for_user(user_id)
2084+
)
2085+
2086+
# Filter down to the relevant rooms
2087+
account_data_by_room_map = {
2088+
room_id: account_data_map
2089+
for room_id, account_data_map in account_data_by_room_map.items()
2090+
if room_id in relevant_room_ids
2091+
}
2092+
2093+
return SlidingSyncResult.Extensions.AccountDataExtension(
2094+
global_account_data_map=global_account_data_map,
2095+
account_data_by_room_map=account_data_by_room_map,
2096+
)

synapse/rest/client/sync.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -929,7 +929,6 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
929929

930930
return 200, response_content
931931

932-
# TODO: Is there a better way to encode things?
933932
async def encode_response(
934933
self,
935934
requester: Requester,
@@ -1117,6 +1116,24 @@ async def encode_extensions(
11171116
extensions.e2ee.device_list_updates.left
11181117
)
11191118

1119+
if extensions.account_data is not None:
1120+
serialized_extensions["account_data"] = {
1121+
# Same as the the top-level `account_data.events` field in Sync v2.
1122+
"global": [
1123+
{"type": account_data_type, "content": content}
1124+
for account_data_type, content in extensions.account_data.global_account_data_map.items()
1125+
],
1126+
# Same as the joined room's account_data field in Sync v2, e.g the path
1127+
# `rooms.join["!foo:bar"].account_data.events`.
1128+
"rooms": {
1129+
room_id: [
1130+
{"type": account_data_type, "content": content}
1131+
for account_data_type, content in event_map.items()
1132+
]
1133+
for room_id, event_map in extensions.account_data.account_data_by_room_map.items()
1134+
},
1135+
}
1136+
11201137
return serialized_extensions
11211138

11221139

synapse/types/handlers/__init__.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,31 @@ def __bool__(self) -> bool:
330330
or self.device_unused_fallback_key_types
331331
)
332332

333+
@attr.s(slots=True, frozen=True, auto_attribs=True)
334+
class AccountDataExtension:
335+
"""The Account Data extension (MSC3959)
336+
337+
Attributes:
338+
global_account_data_map: Mapping from `type` to `content` of global account
339+
data events.
340+
account_data_by_room_map: Mapping from room_id to mapping of `type` to
341+
`content` of room account data events.
342+
"""
343+
344+
global_account_data_map: Mapping[str, JsonMapping]
345+
account_data_by_room_map: Mapping[str, Mapping[str, JsonMapping]]
346+
347+
def __bool__(self) -> bool:
348+
return bool(
349+
self.global_account_data_map or self.account_data_by_room_map
350+
)
351+
333352
to_device: Optional[ToDeviceExtension] = None
334353
e2ee: Optional[E2eeExtension] = None
354+
account_data: Optional[AccountDataExtension] = None
335355

336356
def __bool__(self) -> bool:
337-
return bool(self.to_device or self.e2ee)
357+
return bool(self.to_device or self.e2ee or self.account_data)
338358

339359
next_pos: SlidingSyncStreamToken
340360
lists: Dict[str, SlidingWindowList]

synapse/types/rest/client/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,26 @@ class E2eeExtension(RequestBodyModel):
322322

323323
enabled: Optional[StrictBool] = False
324324

325+
class AccountDataExtension(RequestBodyModel):
326+
"""The Account Data extension (MSC3959)
327+
328+
Attributes:
329+
enabled
330+
lists: List of list keys (from the Sliding Window API) to apply this
331+
extension to.
332+
rooms: List of room IDs (from the Room Subscription API) to apply this
333+
extension to.
334+
"""
335+
336+
enabled: Optional[StrictBool] = False
337+
# Process all lists defined in the Sliding Window API. (This is the default.)
338+
lists: Optional[List[StrictStr]] = ["*"]
339+
# Process all room subscriptions defined in the Room Subscription API. (This is the default.)
340+
rooms: Optional[List[StrictStr]] = ["*"]
341+
325342
to_device: Optional[ToDeviceExtension] = None
326343
e2ee: Optional[E2eeExtension] = None
344+
account_data: Optional[AccountDataExtension] = None
327345

328346
# mypy workaround via https://github.com/pydantic/pydantic/issues/156#issuecomment-1130883884
329347
if TYPE_CHECKING:

0 commit comments

Comments
 (0)