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

Add a configuration to exclude rooms from sync response #12310

Merged
merged 11 commits into from
Mar 30, 2022
1 change: 1 addition & 0 deletions changelog.d/12310.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a configuration option to remove a specific set of rooms from sync responses.
6 changes: 6 additions & 0 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,12 @@ templates:
#
#custom_template_directory: /path/to/custom/templates/

# List of rooms to exclude from sync responses.
# By default, no room is excluded.
#
#exclude_rooms_from_sync:
# - !foo:example.com


# Message retention policy at the server level.
#
Expand Down
10 changes: 10 additions & 0 deletions synapse/config/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,10 @@ def read_config(self, config, **kwargs):
config.get("use_account_validity_in_account_status") or False
)

self.rooms_to_exclude_from_sync: List[str] = (
config.get("exclude_rooms_from_sync") or []
)

def has_tls_listener(self) -> bool:
return any(listener.tls for listener in self.listeners)

Expand Down Expand Up @@ -1234,6 +1238,12 @@ def generate_config_section(
# information about using custom templates.
#
#custom_template_directory: /path/to/custom/templates/

# List of rooms to exclude from sync responses.
babolivier marked this conversation as resolved.
Show resolved Hide resolved
# By default, no room is excluded.
#
#exclude_rooms_from_sync:
# - !foo:example.com
"""
% locals()
)
Expand Down
23 changes: 17 additions & 6 deletions synapse/handlers/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ def __init__(self, hs: "HomeServer"):
expiry_ms=LAZY_LOADED_MEMBERS_CACHE_MAX_AGE,
)

self.rooms_to_exclude = hs.config.server.rooms_to_exclude_from_sync

async def wait_for_sync_for_user(
self,
requester: Requester,
Expand Down Expand Up @@ -1607,13 +1609,15 @@ async def _generate_sync_entry_for_rooms(
ignored_users = await self.store.ignored_users(user_id)
if since_token:
room_changes = await self._get_rooms_changed(
sync_result_builder, ignored_users
sync_result_builder, ignored_users, self.rooms_to_exclude
)
tags_by_room = await self.store.get_updated_tags(
user_id, since_token.account_data_key
)
else:
room_changes = await self._get_all_rooms(sync_result_builder, ignored_users)
room_changes = await self._get_all_rooms(
sync_result_builder, ignored_users, self.rooms_to_exclude
)
tags_by_room = await self.store.get_tags_for_user(user_id)

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

async def _get_rooms_changed(
self, sync_result_builder: "SyncResultBuilder", ignored_users: FrozenSet[str]
self,
sync_result_builder: "SyncResultBuilder",
ignored_users: FrozenSet[str],
excluded_rooms: List[str],
) -> _RoomChanges:
"""Determine the changes in rooms to report to the user.

Expand Down Expand Up @@ -1721,7 +1728,7 @@ async def _get_rooms_changed(
# _have_rooms_changed. We could keep the results in memory to avoid a
# second query, at the cost of more complicated source code.
membership_change_events = await self.store.get_membership_changes_for_user(
user_id, since_token.room_key, now_token.room_key
user_id, since_token.room_key, now_token.room_key, excluded_rooms
)

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

async def _get_all_rooms(
self, sync_result_builder: "SyncResultBuilder", ignored_users: FrozenSet[str]
self,
sync_result_builder: "SyncResultBuilder",
ignored_users: FrozenSet[str],
ignored_rooms: List[str],
) -> _RoomChanges:
"""Returns entries for all rooms for the user.

Expand All @@ -1933,7 +1943,7 @@ async def _get_all_rooms(
Args:
sync_result_builder
ignored_users: Set of users ignored by user.

ignored_rooms: List of rooms to ignore.
"""

user_id = sync_result_builder.sync_config.user.to_string()
Expand All @@ -1944,6 +1954,7 @@ async def _get_all_rooms(
room_list = await self.store.get_rooms_for_local_user_where_membership_is(
user_id=user_id,
membership_list=Membership.LIST,
excluded_rooms=ignored_rooms,
)

room_entries = []
Expand Down
17 changes: 15 additions & 2 deletions synapse/storage/databases/main/roommember.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,10 @@ async def get_invite_for_local_user_in_room(
return None

async def get_rooms_for_local_user_where_membership_is(
self, user_id: str, membership_list: Collection[str]
self,
user_id: str,
membership_list: Collection[str],
excluded_rooms: Optional[List[str]] = None,
) -> List[RoomsForUser]:
"""Get all the rooms for this *local* user where the membership for this user
matches one in the membership list.
Expand All @@ -372,6 +375,7 @@ async def get_rooms_for_local_user_where_membership_is(
user_id: The user ID.
membership_list: A list of synapse.api.constants.Membership
values which the user must be in.
excluded_rooms: A list of rooms to ignore.

Returns:
The RoomsForUser that the user matches the membership types.
Expand All @@ -384,14 +388,19 @@ async def get_rooms_for_local_user_where_membership_is(
self._get_rooms_for_local_user_where_membership_is_txn,
user_id,
membership_list,
excluded_rooms,
)

# Now we filter out forgotten rooms
forgotten_rooms = await self.get_forgotten_rooms_for_user(user_id)
return [room for room in rooms if room.room_id not in forgotten_rooms]
babolivier marked this conversation as resolved.
Show resolved Hide resolved

def _get_rooms_for_local_user_where_membership_is_txn(
self, txn, user_id: str, membership_list: List[str]
self,
txn,
user_id: str,
membership_list: List[str],
excluded_rooms: Optional[List[str]] = None,
) -> List[RoomsForUser]:
# Paranoia check.
if not self.hs.is_mine_id(user_id):
Expand All @@ -404,6 +413,10 @@ def _get_rooms_for_local_user_where_membership_is_txn(
self.database_engine, "c.membership", membership_list
)

if excluded_rooms is not None and len(excluded_rooms) > 0:
clause += "AND room_id NOT IN (%s)" % ",".join("?" for _ in excluded_rooms)
args = args + excluded_rooms

sql = """
SELECT room_id, e.sender, c.membership, event_id, e.stream_ordering, r.room_version
FROM local_current_membership AS c
Expand Down
30 changes: 20 additions & 10 deletions synapse/storage/databases/main/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"""

import logging
from typing import TYPE_CHECKING, Collection, Dict, List, Optional, Set, Tuple
from typing import TYPE_CHECKING, Any, Collection, Dict, List, Optional, Set, Tuple

import attr
from frozendict import frozendict
Expand Down Expand Up @@ -585,7 +585,11 @@ def f(txn: LoggingTransaction) -> List[_EventDictReturn]:
return ret, key

async def get_membership_changes_for_user(
self, user_id: str, from_key: RoomStreamToken, to_key: RoomStreamToken
self,
user_id: str,
from_key: RoomStreamToken,
to_key: RoomStreamToken,
excluded_rooms: Optional[List[str]] = None,
) -> List[EventBase]:
"""Fetch membership events for a given user.

Expand All @@ -610,23 +614,29 @@ def f(txn: LoggingTransaction) -> List[_EventDictReturn]:
min_from_id = from_key.stream
max_to_id = to_key.get_max_stream_pos()

args: List[Any] = [user_id, min_from_id, max_to_id]

ignore_room_clause = ""
if excluded_rooms is not None and len(excluded_rooms) > 0:
ignore_room_clause = "AND e.room_id NOT IN (%s)" % ",".join(
"?" for _ in excluded_rooms
)
args = args + excluded_rooms

sql = """
SELECT m.event_id, instance_name, topological_ordering, stream_ordering
FROM events AS e, room_memberships AS m
WHERE e.event_id = m.event_id
AND m.user_id = ?
AND e.stream_ordering > ? AND e.stream_ordering <= ?
%s
ORDER BY e.stream_ordering ASC
"""
txn.execute(
sql,
(
user_id,
min_from_id,
max_to_id,
),
""" % (
ignore_room_clause,
)

txn.execute(sql, args)

rows = [
_EventDictReturn(event_id, None, stream_ordering)
for event_id, instance_name, topological_ordering, stream_ordering in txn
Expand Down
62 changes: 62 additions & 0 deletions tests/rest/client/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,3 +772,65 @@ def test_user_with_no_rooms_receives_self_device_list_updates(self) -> None:
self.assertIn(
self.user_id, device_list_changes, incremental_sync_channel.json_body
)


class ExcludeRoomTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
sync.register_servlets,
room.register_servlets,
]

def prepare(
self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
) -> None:
self.user_id = self.register_user("user", "password")
self.tok = self.login("user", "password")

self.excluded_room_id = self.helper.create_room_as(self.user_id, tok=self.tok)
self.included_room_id = self.helper.create_room_as(self.user_id, tok=self.tok)

# We need to manually append the room ID, because we can't know the ID before
# creating the room, and we can't set the config after starting the homeserver.
self.hs.get_sync_handler().rooms_to_exclude.append(self.excluded_room_id)

def test_join_leave(self) -> None:
"""Tests that rooms are correctly excluded from the 'join' and 'leave' sections of
sync responses.
"""
channel = self.make_request("GET", "/sync", access_token=self.tok)
self.assertEqual(channel.code, 200, channel.result)

self.assertNotIn(self.excluded_room_id, channel.json_body["rooms"]["join"])
self.assertIn(self.included_room_id, channel.json_body["rooms"]["join"])

self.helper.leave(self.excluded_room_id, self.user_id, tok=self.tok)
self.helper.leave(self.included_room_id, self.user_id, tok=self.tok)

channel = self.make_request(
"GET",
"/sync?since=" + channel.json_body["next_batch"],
access_token=self.tok,
)
self.assertEqual(channel.code, 200, channel.result)

self.assertNotIn(self.excluded_room_id, channel.json_body["rooms"]["leave"])
self.assertIn(self.included_room_id, channel.json_body["rooms"]["leave"])

def test_invite(self) -> None:
"""Tests that rooms are correctly excluded from the 'invite' section of sync
responses.
"""
invitee = self.register_user("invitee", "password")
invitee_tok = self.login("invitee", "password")

self.helper.invite(self.excluded_room_id, self.user_id, invitee, tok=self.tok)
self.helper.invite(self.included_room_id, self.user_id, invitee, tok=self.tok)

channel = self.make_request("GET", "/sync", access_token=invitee_tok)
self.assertEqual(channel.code, 200, channel.result)

self.assertNotIn(self.excluded_room_id, channel.json_body["rooms"]["invite"])
self.assertIn(self.included_room_id, channel.json_body["rooms"]["invite"])