Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 437a8ed

Browse files
authored
Add a configuration to exclude rooms from sync response (#12310)
1 parent e0bb268 commit 437a8ed

File tree

7 files changed

+138
-21
lines changed

7 files changed

+138
-21
lines changed

changelog.d/12310.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a configuration option to remove a specific set of rooms from sync responses.

docs/sample_config.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,15 @@ templates:
539539
#
540540
#custom_template_directory: /path/to/custom/templates/
541541

542+
# List of rooms to exclude from sync responses. This is useful for server
543+
# administrators wishing to group users into a room without these users being able
544+
# to see it from their client.
545+
#
546+
# By default, no room is excluded.
547+
#
548+
#exclude_rooms_from_sync:
549+
# - !foo:example.com
550+
542551

543552
# Message retention policy at the server level.
544553
#

synapse/config/server.py

+13
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,10 @@ def read_config(self, config, **kwargs):
680680
config.get("use_account_validity_in_account_status") or False
681681
)
682682

683+
self.rooms_to_exclude_from_sync: List[str] = (
684+
config.get("exclude_rooms_from_sync") or []
685+
)
686+
683687
def has_tls_listener(self) -> bool:
684688
return any(listener.tls for listener in self.listeners)
685689

@@ -1234,6 +1238,15 @@ def generate_config_section(
12341238
# information about using custom templates.
12351239
#
12361240
#custom_template_directory: /path/to/custom/templates/
1241+
1242+
# List of rooms to exclude from sync responses. This is useful for server
1243+
# administrators wishing to group users into a room without these users being able
1244+
# to see it from their client.
1245+
#
1246+
# By default, no room is excluded.
1247+
#
1248+
#exclude_rooms_from_sync:
1249+
# - !foo:example.com
12371250
"""
12381251
% locals()
12391252
)

synapse/handlers/sync.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,8 @@ def __init__(self, hs: "HomeServer"):
298298
expiry_ms=LAZY_LOADED_MEMBERS_CACHE_MAX_AGE,
299299
)
300300

301+
self.rooms_to_exclude = hs.config.server.rooms_to_exclude_from_sync
302+
301303
async def wait_for_sync_for_user(
302304
self,
303305
requester: Requester,
@@ -1607,13 +1609,15 @@ async def _generate_sync_entry_for_rooms(
16071609
ignored_users = await self.store.ignored_users(user_id)
16081610
if since_token:
16091611
room_changes = await self._get_rooms_changed(
1610-
sync_result_builder, ignored_users
1612+
sync_result_builder, ignored_users, self.rooms_to_exclude
16111613
)
16121614
tags_by_room = await self.store.get_updated_tags(
16131615
user_id, since_token.account_data_key
16141616
)
16151617
else:
1616-
room_changes = await self._get_all_rooms(sync_result_builder, ignored_users)
1618+
room_changes = await self._get_all_rooms(
1619+
sync_result_builder, ignored_users, self.rooms_to_exclude
1620+
)
16171621
tags_by_room = await self.store.get_tags_for_user(user_id)
16181622

16191623
log_kv({"rooms_changed": len(room_changes.room_entries)})
@@ -1689,7 +1693,10 @@ async def _have_rooms_changed(
16891693
return False
16901694

16911695
async def _get_rooms_changed(
1692-
self, sync_result_builder: "SyncResultBuilder", ignored_users: FrozenSet[str]
1696+
self,
1697+
sync_result_builder: "SyncResultBuilder",
1698+
ignored_users: FrozenSet[str],
1699+
excluded_rooms: List[str],
16931700
) -> _RoomChanges:
16941701
"""Determine the changes in rooms to report to the user.
16951702
@@ -1721,7 +1728,7 @@ async def _get_rooms_changed(
17211728
# _have_rooms_changed. We could keep the results in memory to avoid a
17221729
# second query, at the cost of more complicated source code.
17231730
membership_change_events = await self.store.get_membership_changes_for_user(
1724-
user_id, since_token.room_key, now_token.room_key
1731+
user_id, since_token.room_key, now_token.room_key, excluded_rooms
17251732
)
17261733

17271734
mem_change_events_by_room_id: Dict[str, List[EventBase]] = {}
@@ -1922,7 +1929,10 @@ async def _get_rooms_changed(
19221929
)
19231930

19241931
async def _get_all_rooms(
1925-
self, sync_result_builder: "SyncResultBuilder", ignored_users: FrozenSet[str]
1932+
self,
1933+
sync_result_builder: "SyncResultBuilder",
1934+
ignored_users: FrozenSet[str],
1935+
ignored_rooms: List[str],
19261936
) -> _RoomChanges:
19271937
"""Returns entries for all rooms for the user.
19281938
@@ -1933,7 +1943,7 @@ async def _get_all_rooms(
19331943
Args:
19341944
sync_result_builder
19351945
ignored_users: Set of users ignored by user.
1936-
1946+
ignored_rooms: List of rooms to ignore.
19371947
"""
19381948

19391949
user_id = sync_result_builder.sync_config.user.to_string()
@@ -1944,6 +1954,7 @@ async def _get_all_rooms(
19441954
room_list = await self.store.get_rooms_for_local_user_where_membership_is(
19451955
user_id=user_id,
19461956
membership_list=Membership.LIST,
1957+
excluded_rooms=ignored_rooms,
19471958
)
19481959

19491960
room_entries = []

synapse/storage/databases/main/roommember.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,10 @@ async def get_invite_for_local_user_in_room(
361361
return None
362362

363363
async def get_rooms_for_local_user_where_membership_is(
364-
self, user_id: str, membership_list: Collection[str]
364+
self,
365+
user_id: str,
366+
membership_list: Collection[str],
367+
excluded_rooms: Optional[List[str]] = None,
365368
) -> List[RoomsForUser]:
366369
"""Get all the rooms for this *local* user where the membership for this user
367370
matches one in the membership list.
@@ -372,6 +375,7 @@ async def get_rooms_for_local_user_where_membership_is(
372375
user_id: The user ID.
373376
membership_list: A list of synapse.api.constants.Membership
374377
values which the user must be in.
378+
excluded_rooms: A list of rooms to ignore.
375379
376380
Returns:
377381
The RoomsForUser that the user matches the membership types.
@@ -386,12 +390,19 @@ async def get_rooms_for_local_user_where_membership_is(
386390
membership_list,
387391
)
388392

389-
# Now we filter out forgotten rooms
390-
forgotten_rooms = await self.get_forgotten_rooms_for_user(user_id)
391-
return [room for room in rooms if room.room_id not in forgotten_rooms]
393+
# Now we filter out forgotten and excluded rooms
394+
rooms_to_exclude: Set[str] = await self.get_forgotten_rooms_for_user(user_id)
395+
396+
if excluded_rooms is not None:
397+
rooms_to_exclude.update(set(excluded_rooms))
398+
399+
return [room for room in rooms if room.room_id not in rooms_to_exclude]
392400

393401
def _get_rooms_for_local_user_where_membership_is_txn(
394-
self, txn, user_id: str, membership_list: List[str]
402+
self,
403+
txn,
404+
user_id: str,
405+
membership_list: List[str],
395406
) -> List[RoomsForUser]:
396407
# Paranoia check.
397408
if not self.hs.is_mine_id(user_id):

synapse/storage/databases/main/stream.py

+20-10
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"""
3737

3838
import logging
39-
from typing import TYPE_CHECKING, Collection, Dict, List, Optional, Set, Tuple
39+
from typing import TYPE_CHECKING, Any, Collection, Dict, List, Optional, Set, Tuple
4040

4141
import attr
4242
from frozendict import frozendict
@@ -585,7 +585,11 @@ def f(txn: LoggingTransaction) -> List[_EventDictReturn]:
585585
return ret, key
586586

587587
async def get_membership_changes_for_user(
588-
self, user_id: str, from_key: RoomStreamToken, to_key: RoomStreamToken
588+
self,
589+
user_id: str,
590+
from_key: RoomStreamToken,
591+
to_key: RoomStreamToken,
592+
excluded_rooms: Optional[List[str]] = None,
589593
) -> List[EventBase]:
590594
"""Fetch membership events for a given user.
591595
@@ -610,23 +614,29 @@ def f(txn: LoggingTransaction) -> List[_EventDictReturn]:
610614
min_from_id = from_key.stream
611615
max_to_id = to_key.get_max_stream_pos()
612616

617+
args: List[Any] = [user_id, min_from_id, max_to_id]
618+
619+
ignore_room_clause = ""
620+
if excluded_rooms is not None and len(excluded_rooms) > 0:
621+
ignore_room_clause = "AND e.room_id NOT IN (%s)" % ",".join(
622+
"?" for _ in excluded_rooms
623+
)
624+
args = args + excluded_rooms
625+
613626
sql = """
614627
SELECT m.event_id, instance_name, topological_ordering, stream_ordering
615628
FROM events AS e, room_memberships AS m
616629
WHERE e.event_id = m.event_id
617630
AND m.user_id = ?
618631
AND e.stream_ordering > ? AND e.stream_ordering <= ?
632+
%s
619633
ORDER BY e.stream_ordering ASC
620-
"""
621-
txn.execute(
622-
sql,
623-
(
624-
user_id,
625-
min_from_id,
626-
max_to_id,
627-
),
634+
""" % (
635+
ignore_room_clause,
628636
)
629637

638+
txn.execute(sql, args)
639+
630640
rows = [
631641
_EventDictReturn(event_id, None, stream_ordering)
632642
for event_id, instance_name, topological_ordering, stream_ordering in txn

tests/rest/client/test_sync.py

+62
Original file line numberDiff line numberDiff line change
@@ -772,3 +772,65 @@ def test_user_with_no_rooms_receives_self_device_list_updates(self) -> None:
772772
self.assertIn(
773773
self.user_id, device_list_changes, incremental_sync_channel.json_body
774774
)
775+
776+
777+
class ExcludeRoomTestCase(unittest.HomeserverTestCase):
778+
779+
servlets = [
780+
synapse.rest.admin.register_servlets,
781+
login.register_servlets,
782+
sync.register_servlets,
783+
room.register_servlets,
784+
]
785+
786+
def prepare(
787+
self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
788+
) -> None:
789+
self.user_id = self.register_user("user", "password")
790+
self.tok = self.login("user", "password")
791+
792+
self.excluded_room_id = self.helper.create_room_as(self.user_id, tok=self.tok)
793+
self.included_room_id = self.helper.create_room_as(self.user_id, tok=self.tok)
794+
795+
# We need to manually append the room ID, because we can't know the ID before
796+
# creating the room, and we can't set the config after starting the homeserver.
797+
self.hs.get_sync_handler().rooms_to_exclude.append(self.excluded_room_id)
798+
799+
def test_join_leave(self) -> None:
800+
"""Tests that rooms are correctly excluded from the 'join' and 'leave' sections of
801+
sync responses.
802+
"""
803+
channel = self.make_request("GET", "/sync", access_token=self.tok)
804+
self.assertEqual(channel.code, 200, channel.result)
805+
806+
self.assertNotIn(self.excluded_room_id, channel.json_body["rooms"]["join"])
807+
self.assertIn(self.included_room_id, channel.json_body["rooms"]["join"])
808+
809+
self.helper.leave(self.excluded_room_id, self.user_id, tok=self.tok)
810+
self.helper.leave(self.included_room_id, self.user_id, tok=self.tok)
811+
812+
channel = self.make_request(
813+
"GET",
814+
"/sync?since=" + channel.json_body["next_batch"],
815+
access_token=self.tok,
816+
)
817+
self.assertEqual(channel.code, 200, channel.result)
818+
819+
self.assertNotIn(self.excluded_room_id, channel.json_body["rooms"]["leave"])
820+
self.assertIn(self.included_room_id, channel.json_body["rooms"]["leave"])
821+
822+
def test_invite(self) -> None:
823+
"""Tests that rooms are correctly excluded from the 'invite' section of sync
824+
responses.
825+
"""
826+
invitee = self.register_user("invitee", "password")
827+
invitee_tok = self.login("invitee", "password")
828+
829+
self.helper.invite(self.excluded_room_id, self.user_id, invitee, tok=self.tok)
830+
self.helper.invite(self.included_room_id, self.user_id, invitee, tok=self.tok)
831+
832+
channel = self.make_request("GET", "/sync", access_token=invitee_tok)
833+
self.assertEqual(channel.code, 200, channel.result)
834+
835+
self.assertNotIn(self.excluded_room_id, channel.json_body["rooms"]["invite"])
836+
self.assertIn(self.included_room_id, channel.json_body["rooms"]["invite"])

0 commit comments

Comments
 (0)