From ec91bea0bf8699ca6a9d29a43c7c5247c5cd3c85 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 24 Oct 2024 11:47:34 +0100 Subject: [PATCH 01/22] Add event constants. --- synapse/api/constants.py | 2 ++ synapse/config/experimental.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 8db302b3d8b..73ee9eea6ed 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -135,6 +135,8 @@ class EventTypes: PollStart: Final = "m.poll.start" + MSC4171FunctionalMembers: Final = "io.element.functional_members" + class ToDeviceEventTypes: RoomKeyRequest: Final = "m.room_key_request" diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index fd14db0d024..4edbe4de2a9 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -448,5 +448,8 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None: # MSC4151: Report room API (Client-Server API) self.msc4151_enabled: bool = experimental.get("msc4151_enabled", False) + # MSC4171: Service members + self.msc4171_enabled: bool = experimental.get("msc4171_enabled", False) + # MSC4210: Remove legacy mentions self.msc4210_enabled: bool = experimental.get("msc4210_enabled", False) From 027a86cbed796adac41a0763184ce52f18156d7e Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 24 Oct 2024 11:48:40 +0100 Subject: [PATCH 02/22] Filter out service_members from heros set. --- synapse/handlers/sliding_sync/__init__.py | 27 ++++++++++++++++---- synapse/handlers/sync.py | 27 +++++++++++++++++--- synapse/storage/databases/main/roommember.py | 11 ++++---- 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/synapse/handlers/sliding_sync/__init__.py b/synapse/handlers/sliding_sync/__init__.py index a1a6728fb93..80e48cb4cb7 100644 --- a/synapse/handlers/sliding_sync/__init__.py +++ b/synapse/handlers/sliding_sync/__init__.py @@ -90,6 +90,7 @@ def __init__(self, hs: "HomeServer"): self.event_sources = hs.get_event_sources() self.relations_handler = hs.get_relations_handler() self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync + self.hide_service_members_from_heroes = hs.config.experimental.msc4171_enabled self.is_mine_id = hs.is_mine_id self.connection_store = SlidingSyncConnectionStore(self.store) @@ -765,15 +766,31 @@ async def get_room_sync_data( membership_changed = False name_changed = False avatar_changed = False + ignore_members_for_heroes = [] if initial: - # Check whether the room has a name set - name_state_ids = await self.get_current_state_ids_at( + state_filter_types = [(EventTypes.Name, "")] + + if self.hide_service_members_from_heroes: + state_filter_types.append((EventTypes.MSC4171FunctionalMembers, "")) + + # Check whether the room has a name set (and fetch the service members) + state_ids = await self.get_current_state_ids_at( room_id=room_id, room_membership_for_user_at_to_token=room_membership_for_user_at_to_token, - state_filter=StateFilter.from_types([(EventTypes.Name, "")]), + state_filter=StateFilter.from_types(state_filter_types), to_token=to_token, ) - name_event_id = name_state_ids.get((EventTypes.Name, "")) + name_event_id = state_ids.get((EventTypes.Name, "")) + + if self.hide_service_members_from_heroes: + functional_members_id = state_ids.get((EventTypes.MSC4171FunctionalMembers, "")) + if functional_members_id: + functional_members = await self.store.get_event( + functional_members_id, allow_none=True + ) + if functional_members and isinstance(functional_members.content.get("service_members"), list): + ignore_members_for_heroes = functional_members.content.get("service_members") + else: assert from_bound is not None @@ -833,7 +850,7 @@ async def get_room_sync_data( # TODO: Reverse/rewind back to the `to_token` hero_user_ids = extract_heroes_from_room_summary( - room_membership_summary, me=user.to_string() + room_membership_summary, me=user.to_string(), skip_user_ids=ignore_members_for_heroes ) # Fetch the membership counts for rooms we're joined to. diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index f4ea90fbd78..38717cf3f03 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -366,6 +366,7 @@ def __init__(self, hs: "HomeServer"): ) self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync + self.hide_service_members_from_heroes = hs.config.experimental.msc4171_enabled @overload async def wait_for_sync_for_user( @@ -1032,11 +1033,15 @@ async def compute_summary( return None last_event = last_events[-1] + + state_filter_types = [(EventTypes.Name, ""), (EventTypes.CanonicalAlias, "")] + + if self.hide_service_members_from_heroes: + state_filter_types.append((EventTypes.MSC4171FunctionalMembers, "")) + state_ids = await self._state_storage_controller.get_state_ids_for_event( last_event.event_id, - state_filter=StateFilter.from_types( - [(EventTypes.Name, ""), (EventTypes.CanonicalAlias, "")] - ), + state_filter=StateFilter.from_types(state_filter_types), ) # this is heavily cached, thus: fast. @@ -1045,6 +1050,7 @@ async def compute_summary( name_id = state_ids.get((EventTypes.Name, "")) canonical_alias_id = state_ids.get((EventTypes.CanonicalAlias, "")) + summary: JsonDict = {} empty_ms = MemberSummary([], 0) @@ -1069,6 +1075,18 @@ async def compute_summary( if canonical_alias and canonical_alias.content.get("alias"): return summary + + ignore_members_for_heroes = [] + + if self.hide_service_members_from_heroes: + functional_members_id = state_ids.get((EventTypes.MSC4171FunctionalMembers, "")) + if functional_members_id: + functional_members = await self.store.get_event( + functional_members_id, allow_none=True + ) + if functional_members and isinstance(functional_members.content.get("service_members"), list): + ignore_members_for_heroes = functional_members.content.get("service_members") + # FIXME: only build up a member_ids list for our heroes member_ids = {} for membership in ( @@ -1081,7 +1099,8 @@ async def compute_summary( member_ids[user_id] = event_id me = sync_config.user.to_string() - summary["m.heroes"] = extract_heroes_from_room_summary(details, me) + # HERE: + summary["m.heroes"] = extract_heroes_from_room_summary(details, me, ignore_members_for_heroes) if not sync_config.filter_collection.lazy_load_members(): return summary diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index 4249cf77e55..c6263ddf2cb 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -1754,7 +1754,7 @@ def __init__( def extract_heroes_from_room_summary( - details: Mapping[str, MemberSummary], me: str + details: Mapping[str, MemberSummary], me: str, skip_user_ids: List[str] = [], ) -> List[str]: """Determine the users that represent a room, from the perspective of the `me` user. @@ -1770,20 +1770,21 @@ def extract_heroes_from_room_summary( details: Mapping from membership type to member summary. We expect `MemberSummary.members` to already be sorted by `stream_ordering`. me: The user for whom we are determining the heroes for. + skip_user_ids: Users to always ignore when building up a list of heros. Returns a list (possibly empty) of heroes' mxids. """ empty_ms = MemberSummary([], 0) joined_user_ids = [ - r[0] for r in details.get(Membership.JOIN, empty_ms).members if r[0] != me + r[0] for r in details.get(Membership.JOIN, empty_ms).members if r[0] != me and r[0] not in skip_user_ids ] invited_user_ids = [ - r[0] for r in details.get(Membership.INVITE, empty_ms).members if r[0] != me + r[0] for r in details.get(Membership.INVITE, empty_ms).members if r[0] != me and r[0] not in skip_user_ids ] gone_user_ids = [ - r[0] for r in details.get(Membership.LEAVE, empty_ms).members if r[0] != me - ] + [r[0] for r in details.get(Membership.BAN, empty_ms).members if r[0] != me] + r[0] for r in details.get(Membership.LEAVE, empty_ms).members if r[0] != me and r[0] not in skip_user_ids + ] + [r[0] for r in details.get(Membership.BAN, empty_ms).members if r[0] != me and r[0] not in skip_user_ids] # We expect `MemberSummary.members` to already be sorted by `stream_ordering` if joined_user_ids or invited_user_ids: From 587b68753e29d710f00d0638c6c0a4b02583fbba Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 24 Oct 2024 11:51:55 +0100 Subject: [PATCH 03/22] Add comments. --- synapse/handlers/sliding_sync/__init__.py | 1 + synapse/handlers/sync.py | 1 + 2 files changed, 2 insertions(+) diff --git a/synapse/handlers/sliding_sync/__init__.py b/synapse/handlers/sliding_sync/__init__.py index 80e48cb4cb7..aa81a0b6a1c 100644 --- a/synapse/handlers/sliding_sync/__init__.py +++ b/synapse/handlers/sliding_sync/__init__.py @@ -788,6 +788,7 @@ async def get_room_sync_data( functional_members = await self.store.get_event( functional_members_id, allow_none=True ) + # If there is a functional members event, and the service_members is an array, then apply the filter. if functional_members and isinstance(functional_members.content.get("service_members"), list): ignore_members_for_heroes = functional_members.content.get("service_members") diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 38717cf3f03..bcc1796367f 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1084,6 +1084,7 @@ async def compute_summary( functional_members = await self.store.get_event( functional_members_id, allow_none=True ) + # If there is a functional members event, and the service_members is an array, then apply the filter. if functional_members and isinstance(functional_members.content.get("service_members"), list): ignore_members_for_heroes = functional_members.content.get("service_members") From d9c52b27ef8f1f6d6839c60a8c80ecbbe3c93263 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 24 Oct 2024 13:02:27 +0100 Subject: [PATCH 04/22] cleanup --- synapse/handlers/sync.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index bcc1796367f..3e235b6fc8c 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1050,7 +1050,6 @@ async def compute_summary( name_id = state_ids.get((EventTypes.Name, "")) canonical_alias_id = state_ids.get((EventTypes.CanonicalAlias, "")) - summary: JsonDict = {} empty_ms = MemberSummary([], 0) @@ -1100,7 +1099,6 @@ async def compute_summary( member_ids[user_id] = event_id me = sync_config.user.to_string() - # HERE: summary["m.heroes"] = extract_heroes_from_room_summary(details, me, ignore_members_for_heroes) if not sync_config.filter_collection.lazy_load_members(): From 656996a9f5d1f2e2e0e8b9a924dafb197fd1faa9 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 24 Oct 2024 13:31:45 +0100 Subject: [PATCH 05/22] lint --- synapse/handlers/sliding_sync/__init__.py | 16 ++++++++++---- synapse/handlers/sync.py | 17 ++++++++++----- synapse/storage/databases/main/roommember.py | 23 +++++++++++++++----- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/synapse/handlers/sliding_sync/__init__.py b/synapse/handlers/sliding_sync/__init__.py index aa81a0b6a1c..d63284ae382 100644 --- a/synapse/handlers/sliding_sync/__init__.py +++ b/synapse/handlers/sliding_sync/__init__.py @@ -783,14 +783,20 @@ async def get_room_sync_data( name_event_id = state_ids.get((EventTypes.Name, "")) if self.hide_service_members_from_heroes: - functional_members_id = state_ids.get((EventTypes.MSC4171FunctionalMembers, "")) + functional_members_id = state_ids.get( + (EventTypes.MSC4171FunctionalMembers, "") + ) if functional_members_id: functional_members = await self.store.get_event( functional_members_id, allow_none=True ) # If there is a functional members event, and the service_members is an array, then apply the filter. - if functional_members and isinstance(functional_members.content.get("service_members"), list): - ignore_members_for_heroes = functional_members.content.get("service_members") + if functional_members and isinstance( + functional_members.content.get("service_members"), list + ): + ignore_members_for_heroes = functional_members.content.get( + "service_members" + ) else: assert from_bound is not None @@ -851,7 +857,9 @@ async def get_room_sync_data( # TODO: Reverse/rewind back to the `to_token` hero_user_ids = extract_heroes_from_room_summary( - room_membership_summary, me=user.to_string(), skip_user_ids=ignore_members_for_heroes + room_membership_summary, + me=user.to_string(), + skip_user_ids=ignore_members_for_heroes, ) # Fetch the membership counts for rooms we're joined to. diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 3e235b6fc8c..dd8ec06a88c 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1074,18 +1074,23 @@ async def compute_summary( if canonical_alias and canonical_alias.content.get("alias"): return summary - ignore_members_for_heroes = [] if self.hide_service_members_from_heroes: - functional_members_id = state_ids.get((EventTypes.MSC4171FunctionalMembers, "")) + functional_members_id = state_ids.get( + (EventTypes.MSC4171FunctionalMembers, "") + ) if functional_members_id: functional_members = await self.store.get_event( functional_members_id, allow_none=True ) # If there is a functional members event, and the service_members is an array, then apply the filter. - if functional_members and isinstance(functional_members.content.get("service_members"), list): - ignore_members_for_heroes = functional_members.content.get("service_members") + if functional_members and isinstance( + functional_members.content.get("service_members"), list + ): + ignore_members_for_heroes = functional_members.content.get( + "service_members" + ) # FIXME: only build up a member_ids list for our heroes member_ids = {} @@ -1099,7 +1104,9 @@ async def compute_summary( member_ids[user_id] = event_id me = sync_config.user.to_string() - summary["m.heroes"] = extract_heroes_from_room_summary(details, me, ignore_members_for_heroes) + summary["m.heroes"] = extract_heroes_from_room_summary( + details, me, ignore_members_for_heroes + ) if not sync_config.filter_collection.lazy_load_members(): return summary diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index c6263ddf2cb..f276e0c66ef 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -1754,7 +1754,9 @@ def __init__( def extract_heroes_from_room_summary( - details: Mapping[str, MemberSummary], me: str, skip_user_ids: List[str] = [], + details: Mapping[str, MemberSummary], + me: str, + skip_user_ids: List[str] = None, ) -> List[str]: """Determine the users that represent a room, from the perspective of the `me` user. @@ -1775,16 +1777,27 @@ def extract_heroes_from_room_summary( Returns a list (possibly empty) of heroes' mxids. """ empty_ms = MemberSummary([], 0) + skip_user_ids = skip_user_ids or [] joined_user_ids = [ - r[0] for r in details.get(Membership.JOIN, empty_ms).members if r[0] != me and r[0] not in skip_user_ids + r[0] + for r in details.get(Membership.JOIN, empty_ms).members + if r[0] != me and r[0] not in skip_user_ids ] invited_user_ids = [ - r[0] for r in details.get(Membership.INVITE, empty_ms).members if r[0] != me and r[0] not in skip_user_ids + r[0] + for r in details.get(Membership.INVITE, empty_ms).members + if r[0] != me and r[0] not in skip_user_ids ] gone_user_ids = [ - r[0] for r in details.get(Membership.LEAVE, empty_ms).members if r[0] != me and r[0] not in skip_user_ids - ] + [r[0] for r in details.get(Membership.BAN, empty_ms).members if r[0] != me and r[0] not in skip_user_ids] + r[0] + for r in details.get(Membership.LEAVE, empty_ms).members + if r[0] != me and r[0] not in skip_user_ids + ] + [ + r[0] + for r in details.get(Membership.BAN, empty_ms).members + if r[0] != me and r[0] not in skip_user_ids + ] # We expect `MemberSummary.members` to already be sorted by `stream_ordering` if joined_user_ids or invited_user_ids: From 21c4677c0ca73a7f4e9a1f39c0c0a5586e41ea15 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 24 Oct 2024 13:47:58 +0100 Subject: [PATCH 06/22] Add test --- tests/storage/test_roommember.py | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index 330fea0e624..453c21e16ff 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -636,6 +636,49 @@ def test_extract_heroes_from_room_summary_first_five_joins(self) -> None: hero_user_ids, [user1_id, user2_id, user3_id, user4_id, user5_id] ) + def test_extract_heroes_from_room_summary_exclude_service_members(self) -> None: + """ + Test that `extract_heroes_from_room_summary(...)` returns the first 5 joins. + """ + user1_id = self.register_user("user1", "pass") + user1_tok = self.login(user1_id, "pass") + user2_id = self.register_user("user2", "pass") + user2_tok = self.login(user2_id, "pass") + user3_id = self.register_user("user3", "pass") + user3_tok = self.login(user3_id, "pass") + user4_id = self.register_user("user4", "pass") + user4_tok = self.login(user4_id, "pass") + user5_id = self.register_user("user5", "pass") + user5_tok = self.login(user5_id, "pass") + user6_id = self.register_user("user6", "pass") + user6_tok = self.login(user6_id, "pass") + user7_id = self.register_user("user7", "pass") + user7_tok = self.login(user7_id, "pass") + + # Setup the room (user1 is the creator and is joined to the room) + room_id = self.helper.create_room_as(user1_id, tok=user1_tok) + + # User2 -> User7 joins + self.helper.join(room_id, user2_id, tok=user2_tok) + self.helper.join(room_id, user3_id, tok=user3_tok) + self.helper.join(room_id, user4_id, tok=user4_tok) + self.helper.join(room_id, user5_id, tok=user5_tok) + self.helper.join(room_id, user6_id, tok=user6_tok) + self.helper.join(room_id, user7_id, tok=user7_tok) + + room_membership_summary = self.get_success(self.store.get_room_summary(room_id)) + + print(room_membership_summary) + + hero_user_ids = extract_heroes_from_room_summary( + room_membership_summary, me="@fakuser", skip_user_ids=[user2_id, user3_id] + ) + + # First 5 users to join the room + self.assertListEqual( + hero_user_ids, [user1_id, user4_id, user5_id, user6_id, user7_id] + ) + def test_extract_heroes_from_room_summary_membership_order(self) -> None: """ Test that `extract_heroes_from_room_summary(...)` prefers joins/invites over From 7e6ae4f8a69cdb1c67e19394c742797f49aa3f34 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 24 Oct 2024 13:48:31 +0100 Subject: [PATCH 07/22] drop print --- tests/storage/test_roommember.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index 453c21e16ff..e00c5a18955 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -668,8 +668,6 @@ def test_extract_heroes_from_room_summary_exclude_service_members(self) -> None: room_membership_summary = self.get_success(self.store.get_room_summary(room_id)) - print(room_membership_summary) - hero_user_ids = extract_heroes_from_room_summary( room_membership_summary, me="@fakuser", skip_user_ids=[user2_id, user3_id] ) From b13a0325414ad3e50627b40dd573800fb0fd5334 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 24 Oct 2024 14:10:10 +0100 Subject: [PATCH 08/22] Move logic to get_room_summary --- synapse/handlers/sliding_sync/__init__.py | 36 ++------------ synapse/handlers/sync.py | 33 ++----------- synapse/storage/databases/main/roommember.py | 52 ++++++++++++-------- tests/storage/test_roommember.py | 10 +++- 4 files changed, 50 insertions(+), 81 deletions(-) diff --git a/synapse/handlers/sliding_sync/__init__.py b/synapse/handlers/sliding_sync/__init__.py index d63284ae382..a1a6728fb93 100644 --- a/synapse/handlers/sliding_sync/__init__.py +++ b/synapse/handlers/sliding_sync/__init__.py @@ -90,7 +90,6 @@ def __init__(self, hs: "HomeServer"): self.event_sources = hs.get_event_sources() self.relations_handler = hs.get_relations_handler() self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync - self.hide_service_members_from_heroes = hs.config.experimental.msc4171_enabled self.is_mine_id = hs.is_mine_id self.connection_store = SlidingSyncConnectionStore(self.store) @@ -766,38 +765,15 @@ async def get_room_sync_data( membership_changed = False name_changed = False avatar_changed = False - ignore_members_for_heroes = [] if initial: - state_filter_types = [(EventTypes.Name, "")] - - if self.hide_service_members_from_heroes: - state_filter_types.append((EventTypes.MSC4171FunctionalMembers, "")) - - # Check whether the room has a name set (and fetch the service members) - state_ids = await self.get_current_state_ids_at( + # Check whether the room has a name set + name_state_ids = await self.get_current_state_ids_at( room_id=room_id, room_membership_for_user_at_to_token=room_membership_for_user_at_to_token, - state_filter=StateFilter.from_types(state_filter_types), + state_filter=StateFilter.from_types([(EventTypes.Name, "")]), to_token=to_token, ) - name_event_id = state_ids.get((EventTypes.Name, "")) - - if self.hide_service_members_from_heroes: - functional_members_id = state_ids.get( - (EventTypes.MSC4171FunctionalMembers, "") - ) - if functional_members_id: - functional_members = await self.store.get_event( - functional_members_id, allow_none=True - ) - # If there is a functional members event, and the service_members is an array, then apply the filter. - if functional_members and isinstance( - functional_members.content.get("service_members"), list - ): - ignore_members_for_heroes = functional_members.content.get( - "service_members" - ) - + name_event_id = name_state_ids.get((EventTypes.Name, "")) else: assert from_bound is not None @@ -857,9 +833,7 @@ async def get_room_sync_data( # TODO: Reverse/rewind back to the `to_token` hero_user_ids = extract_heroes_from_room_summary( - room_membership_summary, - me=user.to_string(), - skip_user_ids=ignore_members_for_heroes, + room_membership_summary, me=user.to_string() ) # Fetch the membership counts for rooms we're joined to. diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index dd8ec06a88c..f4ea90fbd78 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -366,7 +366,6 @@ def __init__(self, hs: "HomeServer"): ) self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync - self.hide_service_members_from_heroes = hs.config.experimental.msc4171_enabled @overload async def wait_for_sync_for_user( @@ -1033,15 +1032,11 @@ async def compute_summary( return None last_event = last_events[-1] - - state_filter_types = [(EventTypes.Name, ""), (EventTypes.CanonicalAlias, "")] - - if self.hide_service_members_from_heroes: - state_filter_types.append((EventTypes.MSC4171FunctionalMembers, "")) - state_ids = await self._state_storage_controller.get_state_ids_for_event( last_event.event_id, - state_filter=StateFilter.from_types(state_filter_types), + state_filter=StateFilter.from_types( + [(EventTypes.Name, ""), (EventTypes.CanonicalAlias, "")] + ), ) # this is heavily cached, thus: fast. @@ -1074,24 +1069,6 @@ async def compute_summary( if canonical_alias and canonical_alias.content.get("alias"): return summary - ignore_members_for_heroes = [] - - if self.hide_service_members_from_heroes: - functional_members_id = state_ids.get( - (EventTypes.MSC4171FunctionalMembers, "") - ) - if functional_members_id: - functional_members = await self.store.get_event( - functional_members_id, allow_none=True - ) - # If there is a functional members event, and the service_members is an array, then apply the filter. - if functional_members and isinstance( - functional_members.content.get("service_members"), list - ): - ignore_members_for_heroes = functional_members.content.get( - "service_members" - ) - # FIXME: only build up a member_ids list for our heroes member_ids = {} for membership in ( @@ -1104,9 +1081,7 @@ async def compute_summary( member_ids[user_id] = event_id me = sync_config.user.to_string() - summary["m.heroes"] = extract_heroes_from_room_summary( - details, me, ignore_members_for_heroes - ) + summary["m.heroes"] = extract_heroes_from_room_summary(details, me) if not sync_config.filter_collection.lazy_load_members(): return summary diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index f276e0c66ef..9c89d66c056 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -306,7 +306,7 @@ async def get_room_summary(self, room_id: str) -> Mapping[str, MemberSummary]: """ def _get_room_summary_txn( - txn: LoggingTransaction, + txn: LoggingTransaction, exclude_members: List[str] ) -> Dict[str, MemberSummary]: # first get counts. # We do this all in one transaction to keep the cache small. @@ -318,6 +318,10 @@ def _get_room_summary_txn( for membership, count in counts.items(): res.setdefault(membership, MemberSummary([], count)) + exclude_users_clause, args = make_in_list_sql_clause( + self.database_engine, "state_key", exclude_members, negative=True + ) + # Order by membership (joins -> invites -> leave (former insiders) -> # everything else (outsiders like bans/knocks), then by `stream_ordering` so # the first members in the room show up first and to make the sort stable @@ -330,16 +334,18 @@ def _get_room_summary_txn( FROM current_state_events WHERE type = 'm.room.member' AND room_id = ? AND membership IS NOT NULL + AND %s ORDER BY CASE membership WHEN ? THEN 1 WHEN ? THEN 2 WHEN ? THEN 3 ELSE 4 END ASC, event_stream_ordering ASC LIMIT ? - """ + """ % (exclude_users_clause) txn.execute( sql, ( room_id, + *args, # Sort order Membership.JOIN, Membership.INVITE, @@ -357,8 +363,27 @@ def _get_room_summary_txn( return res + functional_members_event_id = await self.db_pool.simple_select_one_onecol( + table="current_state_events", + keyvalues={ + "room_id": room_id, + "type": EventTypes.MSC4171FunctionalMembers, + "state_key": "", + }, + retcol="event_id", + allow_none=True, + ) + + exclude_members = [] + if functional_members_event_id: + functional_members_event = await self.get_event(functional_members_event_id) + # TODO: Validate data + exclude_members = functional_members_event.content.get( + "service_members", [] + ) + return await self.db_pool.runInteraction( - "get_room_summary", _get_room_summary_txn + "get_room_summary", _get_room_summary_txn, exclude_members ) @cached() @@ -1756,7 +1781,6 @@ def __init__( def extract_heroes_from_room_summary( details: Mapping[str, MemberSummary], me: str, - skip_user_ids: List[str] = None, ) -> List[str]: """Determine the users that represent a room, from the perspective of the `me` user. @@ -1772,32 +1796,20 @@ def extract_heroes_from_room_summary( details: Mapping from membership type to member summary. We expect `MemberSummary.members` to already be sorted by `stream_ordering`. me: The user for whom we are determining the heroes for. - skip_user_ids: Users to always ignore when building up a list of heros. Returns a list (possibly empty) of heroes' mxids. """ empty_ms = MemberSummary([], 0) - skip_user_ids = skip_user_ids or [] joined_user_ids = [ - r[0] - for r in details.get(Membership.JOIN, empty_ms).members - if r[0] != me and r[0] not in skip_user_ids + r[0] for r in details.get(Membership.JOIN, empty_ms).members if r[0] != me ] invited_user_ids = [ - r[0] - for r in details.get(Membership.INVITE, empty_ms).members - if r[0] != me and r[0] not in skip_user_ids + r[0] for r in details.get(Membership.INVITE, empty_ms).members if r[0] != me ] gone_user_ids = [ - r[0] - for r in details.get(Membership.LEAVE, empty_ms).members - if r[0] != me and r[0] not in skip_user_ids - ] + [ - r[0] - for r in details.get(Membership.BAN, empty_ms).members - if r[0] != me and r[0] not in skip_user_ids - ] + r[0] for r in details.get(Membership.LEAVE, empty_ms).members if r[0] != me + ] + [r[0] for r in details.get(Membership.BAN, empty_ms).members if r[0] != me] # We expect `MemberSummary.members` to already be sorted by `stream_ordering` if joined_user_ids or invited_user_ids: diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index e00c5a18955..590a95685ff 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -658,6 +658,14 @@ def test_extract_heroes_from_room_summary_exclude_service_members(self) -> None: # Setup the room (user1 is the creator and is joined to the room) room_id = self.helper.create_room_as(user1_id, tok=user1_tok) + # Exclude some users + self.helper.send_state( + room_id, + event_type=EventTypes.MSC4171FunctionalMembers, + body={"service_members": [user2_id, user3_id]}, + tok=user1_tok, + ) + # User2 -> User7 joins self.helper.join(room_id, user2_id, tok=user2_tok) self.helper.join(room_id, user3_id, tok=user3_tok) @@ -669,7 +677,7 @@ def test_extract_heroes_from_room_summary_exclude_service_members(self) -> None: room_membership_summary = self.get_success(self.store.get_room_summary(room_id)) hero_user_ids = extract_heroes_from_room_summary( - room_membership_summary, me="@fakuser", skip_user_ids=[user2_id, user3_id] + room_membership_summary, me="@fakuser" ) # First 5 users to join the room From b84e949eecdbcc0dde7d2bfeeaf15ebfb2e556cc Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 24 Oct 2024 14:13:34 +0100 Subject: [PATCH 09/22] Validate event content --- synapse/storage/databases/main/roommember.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index 9c89d66c056..3c34f29697e 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -377,10 +377,13 @@ def _get_room_summary_txn( exclude_members = [] if functional_members_event_id: functional_members_event = await self.get_event(functional_members_event_id) - # TODO: Validate data - exclude_members = functional_members_event.content.get( - "service_members", [] + functional_members_data = functional_members_event.content.get( + "service_members" ) + # ONLY use this value if this looks like a valid list of strings. Otherwise, ignore. + if isinstance(functional_members_data, list) and all(isinstance(item, str) for item in functional_members_data): + exclude_members = functional_members_data + return await self.db_pool.runInteraction( "get_room_summary", _get_room_summary_txn, exclude_members From 990e2cefd3e941cf97660badb7019ac8da580611 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 24 Oct 2024 14:13:42 +0100 Subject: [PATCH 10/22] lint --- synapse/storage/databases/main/roommember.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index 3c34f29697e..254c89a8c80 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -381,10 +381,11 @@ def _get_room_summary_txn( "service_members" ) # ONLY use this value if this looks like a valid list of strings. Otherwise, ignore. - if isinstance(functional_members_data, list) and all(isinstance(item, str) for item in functional_members_data): + if isinstance(functional_members_data, list) and all( + isinstance(item, str) for item in functional_members_data + ): exclude_members = functional_members_data - return await self.db_pool.runInteraction( "get_room_summary", _get_room_summary_txn, exclude_members ) From 9567c1465b18afafe3ff0a00d0a6e1c029cc8f11 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 24 Oct 2024 14:18:59 +0100 Subject: [PATCH 11/22] changelog --- changelog.d/17866.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/17866.feature diff --git a/changelog.d/17866.feature b/changelog.d/17866.feature new file mode 100644 index 00000000000..222f553ba21 --- /dev/null +++ b/changelog.d/17866.feature @@ -0,0 +1 @@ +Add support for filtering out "service members" from room summary responses, as described in MSC4171. \ No newline at end of file From 0ab5dae3e221248033912689c9cb139d43541326 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 25 Oct 2024 09:36:27 +0100 Subject: [PATCH 12/22] Update comment. --- tests/storage/test_roommember.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index 590a95685ff..d73e1382743 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -680,7 +680,7 @@ def test_extract_heroes_from_room_summary_exclude_service_members(self) -> None: room_membership_summary, me="@fakuser" ) - # First 5 users to join the room + # First 5 users to join the room, excluding service members. self.assertListEqual( hero_user_ids, [user1_id, user4_id, user5_id, user6_id, user7_id] ) From d5530ff6ae9cb9442a9433ea509d1f180670b10c Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 25 Oct 2024 09:37:10 +0100 Subject: [PATCH 13/22] fixup comment --- tests/storage/test_roommember.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index d73e1382743..733df308288 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -638,7 +638,8 @@ def test_extract_heroes_from_room_summary_first_five_joins(self) -> None: def test_extract_heroes_from_room_summary_exclude_service_members(self) -> None: """ - Test that `extract_heroes_from_room_summary(...)` returns the first 5 joins. + Test that `extract_heroes_from_room_summary(...)` returns the first 5 joins who are + not mentioned in the functional members state event. """ user1_id = self.register_user("user1", "pass") user1_tok = self.login(user1_id, "pass") From 0a8d85be86e2f6a2af83181c8027d689cd9fdd94 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 31 Oct 2024 12:14:45 +0000 Subject: [PATCH 14/22] Update tests/storage/test_roommember.py Co-authored-by: Eric Eastwood --- tests/storage/test_roommember.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index 733df308288..e663c949a97 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -678,7 +678,7 @@ def test_extract_heroes_from_room_summary_exclude_service_members(self) -> None: room_membership_summary = self.get_success(self.store.get_room_summary(room_id)) hero_user_ids = extract_heroes_from_room_summary( - room_membership_summary, me="@fakuser" + room_membership_summary, me="@fakeuser" ) # First 5 users to join the room, excluding service members. From 1da2f61935c09de678bf27237285fea288c67b56 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 31 Oct 2024 12:20:59 +0000 Subject: [PATCH 15/22] Update changelog.d/17866.feature Co-authored-by: Eric Eastwood --- changelog.d/17866.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/17866.feature b/changelog.d/17866.feature index 222f553ba21..b7ca02978b9 100644 --- a/changelog.d/17866.feature +++ b/changelog.d/17866.feature @@ -1 +1 @@ -Add support for filtering out "service members" from room summary responses, as described in MSC4171. \ No newline at end of file +Add experimental support for filtering out "service members" from room summary responses, as described in [MSC4171](https://github.com/matrix-org/matrix-spec-proposals/pull/4171). \ No newline at end of file From 5b8cbf40e91f570b6cc49d6aeaf53c2fc635b00f Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 31 Oct 2024 13:34:00 +0000 Subject: [PATCH 16/22] Actually stick behind a feature flag. --- synapse/federation/federation_server.py | 2 + synapse/handlers/sliding_sync/__init__.py | 6 ++- synapse/handlers/sync.py | 5 ++- synapse/storage/databases/main/roommember.py | 46 +++++++++++--------- tests/storage/test_roommember.py | 4 +- 5 files changed, 39 insertions(+), 24 deletions(-) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 1932fa82a4a..aba01f29a34 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -732,6 +732,8 @@ async def on_send_join_request( state_event_ids: Collection[str] servers_in_room: Optional[Collection[str]] if caller_supports_partial_state: + # NOTE: We do not exclude service members from the federated + # room summary. summary = await self.store.get_room_summary(room_id) state_event_ids = _get_event_ids_for_partial_state_join( event, prev_state_ids, summary diff --git a/synapse/handlers/sliding_sync/__init__.py b/synapse/handlers/sliding_sync/__init__.py index a1a6728fb93..49a2f020a1b 100644 --- a/synapse/handlers/sliding_sync/__init__.py +++ b/synapse/handlers/sliding_sync/__init__.py @@ -90,6 +90,8 @@ def __init__(self, hs: "HomeServer"): self.event_sources = hs.get_event_sources() self.relations_handler = hs.get_relations_handler() self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync + self.should_exclude_service_members = hs.config.experimental.msc4171_enabled + self.is_mine_id = hs.is_mine_id self.connection_store = SlidingSyncConnectionStore(self.store) @@ -829,7 +831,9 @@ async def get_room_sync_data( # For invite/knock rooms we don't include the information. room_membership_summary = {} else: - room_membership_summary = await self.store.get_room_summary(room_id) + room_membership_summary = await self.store.get_room_summary( + room_id, self.should_exclude_service_members + ) # TODO: Reverse/rewind back to the `to_token` hero_user_ids = extract_heroes_from_room_summary( diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index f4ea90fbd78..8d50fa05bc1 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -343,6 +343,7 @@ def __init__(self, hs: "HomeServer"): self._task_scheduler = hs.get_task_scheduler() self.should_calculate_push_rules = hs.config.push.enable_push + self.should_exclude_service_members = hs.config.experimental.msc4171_enabled # TODO: flush cache entries on subsequent sync request. # Once we get the next /sync request (ie, one with the same access token @@ -1040,7 +1041,9 @@ async def compute_summary( ) # this is heavily cached, thus: fast. - details = await self.store.get_room_summary(room_id) + details = await self.store.get_room_summary( + room_id, self.should_exclude_service_members + ) name_id = state_ids.get((EventTypes.Name, "")) canonical_alias_id = state_ids.get((EventTypes.CanonicalAlias, "")) diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index 254c89a8c80..adf88425522 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -285,7 +285,9 @@ def _get_users_in_room_with_profiles( ) @cached(max_entries=100000) # type: ignore[synapse-@cached-mutable] - async def get_room_summary(self, room_id: str) -> Mapping[str, MemberSummary]: + async def get_room_summary( + self, room_id: str, exclude_service_users: bool = False + ) -> Mapping[str, MemberSummary]: """ Get the details of a room roughly suitable for use by the room summary extension to /sync. Useful when lazy loading room members. @@ -363,28 +365,30 @@ def _get_room_summary_txn( return res - functional_members_event_id = await self.db_pool.simple_select_one_onecol( - table="current_state_events", - keyvalues={ - "room_id": room_id, - "type": EventTypes.MSC4171FunctionalMembers, - "state_key": "", - }, - retcol="event_id", - allow_none=True, - ) - exclude_members = [] - if functional_members_event_id: - functional_members_event = await self.get_event(functional_members_event_id) - functional_members_data = functional_members_event.content.get( - "service_members" + if exclude_service_users: + functional_members_event_id = await self.db_pool.simple_select_one_onecol( + table="current_state_events", + keyvalues={ + "room_id": room_id, + "type": EventTypes.MSC4171FunctionalMembers, + "state_key": "", + }, + retcol="event_id", + allow_none=True, ) - # ONLY use this value if this looks like a valid list of strings. Otherwise, ignore. - if isinstance(functional_members_data, list) and all( - isinstance(item, str) for item in functional_members_data - ): - exclude_members = functional_members_data + if functional_members_event_id: + functional_members_event = await self.get_event( + functional_members_event_id + ) + functional_members_data = functional_members_event.content.get( + "service_members" + ) + # ONLY use this value if this looks like a valid list of strings. Otherwise, ignore. + if isinstance(functional_members_data, list) and all( + isinstance(item, str) for item in functional_members_data + ): + exclude_members = functional_members_data return await self.db_pool.runInteraction( "get_room_summary", _get_room_summary_txn, exclude_members diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index e663c949a97..2d67532d67f 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -675,7 +675,9 @@ def test_extract_heroes_from_room_summary_exclude_service_members(self) -> None: self.helper.join(room_id, user6_id, tok=user6_tok) self.helper.join(room_id, user7_id, tok=user7_tok) - room_membership_summary = self.get_success(self.store.get_room_summary(room_id)) + room_membership_summary = self.get_success( + self.store.get_room_summary(room_id, True) + ) hero_user_ids = extract_heroes_from_room_summary( room_membership_summary, me="@fakeuser" From e60d4cfc0f434e742f99dc60fc0a85da892f6038 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 31 Oct 2024 13:41:18 +0000 Subject: [PATCH 17/22] Ensure empty array if all users are excluded. --- tests/storage/test_roommember.py | 41 +++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index 2d67532d67f..4ee5776adc0 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -628,7 +628,7 @@ def test_extract_heroes_from_room_summary_first_five_joins(self) -> None: room_membership_summary = self.get_success(self.store.get_room_summary(room_id)) hero_user_ids = extract_heroes_from_room_summary( - room_membership_summary, me="@fakuser" + room_membership_summary, me="@fakeuser" ) # First 5 users to join the room @@ -688,6 +688,45 @@ def test_extract_heroes_from_room_summary_exclude_service_members(self) -> None: hero_user_ids, [user1_id, user4_id, user5_id, user6_id, user7_id] ) + def test_extract_heroes_from_room_summary_exclude_service_members_with_empty_heroes(self) -> None: + """ + Test that `extract_heroes_from_room_summary(...)` will return an + empty set of heroes if all users have been excluded. + """ + user1_id = self.register_user("user1", "pass") + user1_tok = self.login(user1_id, "pass") + user2_id = self.register_user("user2", "pass") + user2_tok = self.login(user2_id, "pass") + user3_id = self.register_user("user3", "pass") + user3_tok = self.login(user3_id, "pass") + + # Setup the room (user1 is the creator and is joined to the room) + room_id = self.helper.create_room_as(user1_id, tok=user1_tok) + + # Exclude all users (except the creator, who is excluded from the results anyway) + self.helper.send_state( + room_id, + event_type=EventTypes.MSC4171FunctionalMembers, + body={"service_members": [user2_id, user3_id]}, + tok=user1_tok, + ) + + self.helper.join(room_id, user2_id, tok=user2_tok) + self.helper.join(room_id, user3_id, tok=user3_tok) + + room_membership_summary = self.get_success( + self.store.get_room_summary(room_id, True) + ) + + hero_user_ids = extract_heroes_from_room_summary( + room_membership_summary, me=user1_id + ) + + # First 5 users to join the room, excluding service members. + self.assertListEqual( + hero_user_ids, [] + ) + def test_extract_heroes_from_room_summary_membership_order(self) -> None: """ Test that `extract_heroes_from_room_summary(...)` prefers joins/invites over From 5a3cdf2c91016f7c552fb467c48e4afd2e3d23a1 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 31 Oct 2024 13:41:28 +0000 Subject: [PATCH 18/22] lint --- tests/storage/test_roommember.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index 4ee5776adc0..c8af9038b36 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -688,7 +688,9 @@ def test_extract_heroes_from_room_summary_exclude_service_members(self) -> None: hero_user_ids, [user1_id, user4_id, user5_id, user6_id, user7_id] ) - def test_extract_heroes_from_room_summary_exclude_service_members_with_empty_heroes(self) -> None: + def test_extract_heroes_from_room_summary_exclude_service_members_with_empty_heroes( + self, + ) -> None: """ Test that `extract_heroes_from_room_summary(...)` will return an empty set of heroes if all users have been excluded. @@ -723,9 +725,7 @@ def test_extract_heroes_from_room_summary_exclude_service_members_with_empty_her ) # First 5 users to join the room, excluding service members. - self.assertListEqual( - hero_user_ids, [] - ) + self.assertListEqual(hero_user_ids, []) def test_extract_heroes_from_room_summary_membership_order(self) -> None: """ From 6c17ee1a349280e37ccd89ce6ffbbebc3689973b Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 31 Oct 2024 13:50:21 +0000 Subject: [PATCH 19/22] docstring --- synapse/storage/databases/main/roommember.py | 1 + 1 file changed, 1 insertion(+) diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index adf88425522..ed45de2a869 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -303,6 +303,7 @@ async def get_room_summary( Args: room_id: The room ID to query + exclude_service_users: Should MSC4171 be used to exclude service members Returns: dict of membership states, pointing to a MemberSummary named tuple. """ From 429381ab05b56f0b35ecf497d12665c5f2977436 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 1 Nov 2024 10:12:53 +0000 Subject: [PATCH 20/22] Add parameter to enable msc on sync requests. --- synapse/handlers/sliding_sync/__init__.py | 4 +++- synapse/handlers/sync.py | 5 ++++- synapse/rest/client/sync.py | 16 +++++++++++++++- tests/handlers/test_sync.py | 1 + 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/sliding_sync/__init__.py b/synapse/handlers/sliding_sync/__init__.py index 49a2f020a1b..21d7feee7bb 100644 --- a/synapse/handlers/sliding_sync/__init__.py +++ b/synapse/handlers/sliding_sync/__init__.py @@ -832,7 +832,9 @@ async def get_room_sync_data( room_membership_summary = {} else: room_membership_summary = await self.store.get_room_summary( - room_id, self.should_exclude_service_members + room_id, + self.should_exclude_service_members + and sync_config.exclude_service_members_from_heroes, ) # TODO: Reverse/rewind back to the `to_token` diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 8d50fa05bc1..5f6c2e79b3f 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -143,6 +143,7 @@ class SyncConfig: filter_collection: FilterCollection is_guest: bool device_id: Optional[str] + exclude_service_members_from_heroes: bool @attr.s(slots=True, frozen=True, auto_attribs=True) @@ -1042,7 +1043,9 @@ async def compute_summary( # this is heavily cached, thus: fast. details = await self.store.get_room_summary( - room_id, self.should_exclude_service_members + room_id, + self.should_exclude_service_members + and sync_config.exclude_service_members_from_heroes, ) name_id = state_ids.get((EventTypes.Name, "")) diff --git a/synapse/rest/client/sync.py b/synapse/rest/client/sync.py index 122708e933c..e4c53722606 100644 --- a/synapse/rest/client/sync.py +++ b/synapse/rest/client/sync.py @@ -151,16 +151,20 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: ) filter_id = parse_string(request, "filter") full_state = parse_boolean(request, "full_state", default=False) + exclude_service_members_from_heroes = parse_boolean( + request, "msc4171_exclude_service_members", default=False + ) logger.debug( "/sync: user=%r, timeout=%r, since=%r, " - "set_presence=%r, filter_id=%r, device_id=%r", + "set_presence=%r, filter_id=%r, device_id=%r, exclude_service_members=%r", user, timeout, since, set_presence, filter_id, device_id, + exclude_service_members_from_heroes, ) # Stream position of the last ignored users account data event for this user, @@ -220,6 +224,7 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: filter_collection=filter_collection, is_guest=requester.is_guest, device_id=device_id, + exclude_service_members_from_heroes=exclude_service_members_from_heroes, ) since_token = None @@ -682,12 +687,16 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: timeout = parse_integer(request, "timeout", default=0) since = parse_string(request, "since") + exclude_service_members_from_heroes = parse_boolean( + request, "msc4171_exclude_service_members", default=False + ) sync_config = SyncConfig( user=user, filter_collection=self.only_member_events_filter_collection, is_guest=requester.is_guest, device_id=device_id, + exclude_service_members_from_heroes=exclude_service_members_from_heroes, ) since_token = None @@ -886,6 +895,10 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: # Position in the stream from_token_string = parse_string(request, "pos") + exclude_service_members_from_heroes = parse_boolean( + request, "msc4171_exclude_service_members", default=False + ) + from_token = None if from_token_string is not None: from_token = await SlidingSyncStreamToken.from_string( @@ -935,6 +948,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: lists=body.lists, room_subscriptions=body.room_subscriptions, extensions=body.extensions, + exclude_service_members_from_heroes=exclude_service_members_from_heroes, ) sliding_sync_results = await self.sliding_sync_handler.wait_for_sync_for_user( diff --git a/tests/handlers/test_sync.py b/tests/handlers/test_sync.py index d7bbc680373..38b89e92459 100644 --- a/tests/handlers/test_sync.py +++ b/tests/handlers/test_sync.py @@ -1079,4 +1079,5 @@ def generate_sync_config( filter_collection=filter_collection, is_guest=False, device_id=device_id, + exclude_service_members_from_heroes=False, ) From ee13e3f4cd04a8288c69ce8eaf92c7b52d35f0e2 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 4 Nov 2024 13:26:50 +0000 Subject: [PATCH 21/22] use format. --- synapse/storage/databases/main/roommember.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index ed45de2a869..f8c112b825f 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -337,12 +337,12 @@ def _get_room_summary_txn( FROM current_state_events WHERE type = 'm.room.member' AND room_id = ? AND membership IS NOT NULL - AND %s + AND {} ORDER BY CASE membership WHEN ? THEN 1 WHEN ? THEN 2 WHEN ? THEN 3 ELSE 4 END ASC, event_stream_ordering ASC LIMIT ? - """ % (exclude_users_clause) + """.format(exclude_users_clause) txn.execute( sql, From 2c181a71142c9b23629890848dda5bd974f12bb7 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 4 Nov 2024 13:28:54 +0000 Subject: [PATCH 22/22] Add exclude_service_members_from_heroes to SlidingSyncConfig --- synapse/types/handlers/sliding_sync.py | 1 + 1 file changed, 1 insertion(+) diff --git a/synapse/types/handlers/sliding_sync.py b/synapse/types/handlers/sliding_sync.py index aae60fddeab..7d27e647156 100644 --- a/synapse/types/handlers/sliding_sync.py +++ b/synapse/types/handlers/sliding_sync.py @@ -68,6 +68,7 @@ class SlidingSyncConfig(SlidingSyncBody): user: UserID requester: Requester + exclude_service_members_from_heroes: bool # Pydantic config class Config: