mirror of
https://mau.dev/maunium/synapse.git
synced 2024-10-01 01:36:05 -04:00
Sliding Sync: Update filters to be robust against remote invite rooms (#17450)
Update `filters.is_encrypted` and `filters.types`/`filters.not_types` to be robust when dealing with remote invite rooms in Sliding Sync. Part of [MSC3575](https://github.com/matrix-org/matrix-spec-proposals/pull/3575): Sliding Sync Follow-up to https://github.com/element-hq/synapse/pull/17434 We now take into account current state, fallback to stripped state for invite/knock rooms, then historical state. If we can't determine the info needed to filter a room (either from state or stripped state), it is filtered out.
This commit is contained in:
parent
b221f0b84b
commit
46de0ee16b
1
changelog.d/17450.bugfix
Normal file
1
changelog.d/17450.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Update experimental [MSC3575](https://github.com/matrix-org/matrix-spec-proposals/pull/3575) Sliding Sync `/sync` endpoint to handle invite/knock rooms when filtering.
|
@ -225,6 +225,11 @@ class EventContentFields:
|
||||
# This is deprecated in MSC2175.
|
||||
ROOM_CREATOR: Final = "creator"
|
||||
|
||||
# The version of the room for `m.room.create` events.
|
||||
ROOM_VERSION: Final = "room_version"
|
||||
|
||||
ROOM_NAME: Final = "name"
|
||||
|
||||
# Used in m.room.guest_access events.
|
||||
GUEST_ACCESS: Final = "guest_access"
|
||||
|
||||
@ -237,6 +242,9 @@ class EventContentFields:
|
||||
# an unspecced field added to to-device messages to identify them uniquely-ish
|
||||
TO_DEVICE_MSGID: Final = "org.matrix.msgid"
|
||||
|
||||
# `m.room.encryption`` algorithm field
|
||||
ENCRYPTION_ALGORITHM: Final = "algorithm"
|
||||
|
||||
|
||||
class EventUnsignedContentFields:
|
||||
"""Fields found inside the 'unsigned' data on events"""
|
||||
|
@ -554,3 +554,22 @@ def relation_from_event(event: EventBase) -> Optional[_EventRelation]:
|
||||
aggregation_key = None
|
||||
|
||||
return _EventRelation(parent_id, rel_type, aggregation_key)
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class StrippedStateEvent:
|
||||
"""
|
||||
A stripped down state event. Usually used for remote invite/knocks so the user can
|
||||
make an informed decision on whether they want to join.
|
||||
|
||||
Attributes:
|
||||
type: Event `type`
|
||||
state_key: Event `state_key`
|
||||
sender: Event `sender`
|
||||
content: Event `content`
|
||||
"""
|
||||
|
||||
type: str
|
||||
state_key: str
|
||||
sender: str
|
||||
content: Dict[str, Any]
|
||||
|
@ -49,7 +49,7 @@ from synapse.api.errors import Codes, SynapseError
|
||||
from synapse.api.room_versions import RoomVersion
|
||||
from synapse.types import JsonDict, Requester
|
||||
|
||||
from . import EventBase, make_event_from_dict
|
||||
from . import EventBase, StrippedStateEvent, make_event_from_dict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.handlers.relations import BundledAggregations
|
||||
@ -854,3 +854,30 @@ def strip_event(event: EventBase) -> JsonDict:
|
||||
"content": event.content,
|
||||
"sender": event.sender,
|
||||
}
|
||||
|
||||
|
||||
def parse_stripped_state_event(raw_stripped_event: Any) -> Optional[StrippedStateEvent]:
|
||||
"""
|
||||
Given a raw value from an event's `unsigned` field, attempt to parse it into a
|
||||
`StrippedStateEvent`.
|
||||
"""
|
||||
if isinstance(raw_stripped_event, dict):
|
||||
# All of these fields are required
|
||||
type = raw_stripped_event.get("type")
|
||||
state_key = raw_stripped_event.get("state_key")
|
||||
sender = raw_stripped_event.get("sender")
|
||||
content = raw_stripped_event.get("content")
|
||||
if (
|
||||
isinstance(type, str)
|
||||
and isinstance(state_key, str)
|
||||
and isinstance(sender, str)
|
||||
and isinstance(content, dict)
|
||||
):
|
||||
return StrippedStateEvent(
|
||||
type=type,
|
||||
state_key=state_key,
|
||||
sender=sender,
|
||||
content=content,
|
||||
)
|
||||
|
||||
return None
|
||||
|
@ -17,6 +17,7 @@
|
||||
# [This file includes modifications made by New Vector Limited]
|
||||
#
|
||||
#
|
||||
import enum
|
||||
import logging
|
||||
from enum import Enum
|
||||
from itertools import chain
|
||||
@ -26,23 +27,35 @@ from typing import (
|
||||
Dict,
|
||||
Final,
|
||||
List,
|
||||
Literal,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
import attr
|
||||
from immutabledict import immutabledict
|
||||
from typing_extensions import assert_never
|
||||
|
||||
from synapse.api.constants import AccountDataTypes, Direction, EventTypes, Membership
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.utils import strip_event
|
||||
from synapse.api.constants import (
|
||||
AccountDataTypes,
|
||||
Direction,
|
||||
EventContentFields,
|
||||
EventTypes,
|
||||
Membership,
|
||||
)
|
||||
from synapse.events import EventBase, StrippedStateEvent
|
||||
from synapse.events.utils import parse_stripped_state_event, strip_event
|
||||
from synapse.handlers.relations import BundledAggregations
|
||||
from synapse.logging.opentracing import log_kv, start_active_span, tag_args, trace
|
||||
from synapse.storage.databases.main.roommember import extract_heroes_from_room_summary
|
||||
from synapse.storage.databases.main.state import (
|
||||
ROOM_UNKNOWN_SENTINEL,
|
||||
Sentinel as StateSentinel,
|
||||
)
|
||||
from synapse.storage.databases.main.stream import CurrentStateDeltaMembership
|
||||
from synapse.storage.roommember import MemberSummary
|
||||
from synapse.types import (
|
||||
@ -50,6 +63,7 @@ from synapse.types import (
|
||||
JsonDict,
|
||||
JsonMapping,
|
||||
MultiWriterStreamToken,
|
||||
MutableStateMap,
|
||||
PersistedEventPosition,
|
||||
Requester,
|
||||
RoomStreamToken,
|
||||
@ -71,6 +85,12 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Sentinel(enum.Enum):
|
||||
# defining a sentinel in this way allows mypy to correctly handle the
|
||||
# type of a dictionary lookup and subsequent type narrowing.
|
||||
UNSET_SENTINEL = object()
|
||||
|
||||
|
||||
# The event types that clients should consider as new activity.
|
||||
DEFAULT_BUMP_EVENT_TYPES = {
|
||||
EventTypes.Create,
|
||||
@ -1172,6 +1192,265 @@ class SlidingSyncHandler:
|
||||
|
||||
# return None
|
||||
|
||||
async def _bulk_get_stripped_state_for_rooms_from_sync_room_map(
|
||||
self,
|
||||
room_ids: StrCollection,
|
||||
sync_room_map: Dict[str, _RoomMembershipForUser],
|
||||
) -> Dict[str, Optional[StateMap[StrippedStateEvent]]]:
|
||||
"""
|
||||
Fetch stripped state for a list of room IDs. Stripped state is only
|
||||
applicable to invite/knock rooms. Other rooms will have `None` as their
|
||||
stripped state.
|
||||
|
||||
For invite rooms, we pull from `unsigned.invite_room_state`.
|
||||
For knock rooms, we pull from `unsigned.knock_room_state`.
|
||||
|
||||
Args:
|
||||
room_ids: Room IDs to fetch stripped state for
|
||||
sync_room_map: Dictionary of room IDs to sort along with membership
|
||||
information in the room at the time of `to_token`.
|
||||
|
||||
Returns:
|
||||
Mapping from room_id to mapping of (type, state_key) to stripped state
|
||||
event.
|
||||
"""
|
||||
room_id_to_stripped_state_map: Dict[
|
||||
str, Optional[StateMap[StrippedStateEvent]]
|
||||
] = {}
|
||||
|
||||
# Fetch what we haven't before
|
||||
room_ids_to_fetch = [
|
||||
room_id
|
||||
for room_id in room_ids
|
||||
if room_id not in room_id_to_stripped_state_map
|
||||
]
|
||||
|
||||
# Gather a list of event IDs we can grab stripped state from
|
||||
invite_or_knock_event_ids: List[str] = []
|
||||
for room_id in room_ids_to_fetch:
|
||||
if sync_room_map[room_id].membership in (
|
||||
Membership.INVITE,
|
||||
Membership.KNOCK,
|
||||
):
|
||||
event_id = sync_room_map[room_id].event_id
|
||||
# If this is an invite/knock then there should be an event_id
|
||||
assert event_id is not None
|
||||
invite_or_knock_event_ids.append(event_id)
|
||||
else:
|
||||
room_id_to_stripped_state_map[room_id] = None
|
||||
|
||||
invite_or_knock_events = await self.store.get_events(invite_or_knock_event_ids)
|
||||
for invite_or_knock_event in invite_or_knock_events.values():
|
||||
room_id = invite_or_knock_event.room_id
|
||||
membership = invite_or_knock_event.membership
|
||||
|
||||
raw_stripped_state_events = None
|
||||
if membership == Membership.INVITE:
|
||||
invite_room_state = invite_or_knock_event.unsigned.get(
|
||||
"invite_room_state"
|
||||
)
|
||||
raw_stripped_state_events = invite_room_state
|
||||
elif membership == Membership.KNOCK:
|
||||
knock_room_state = invite_or_knock_event.unsigned.get(
|
||||
"knock_room_state"
|
||||
)
|
||||
raw_stripped_state_events = knock_room_state
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"Unexpected membership {membership} (this is a problem with Synapse itself)"
|
||||
)
|
||||
|
||||
stripped_state_map: Optional[MutableStateMap[StrippedStateEvent]] = None
|
||||
# Scrutinize unsigned things. `raw_stripped_state_events` should be a list
|
||||
# of stripped events
|
||||
if raw_stripped_state_events is not None:
|
||||
stripped_state_map = {}
|
||||
if isinstance(raw_stripped_state_events, list):
|
||||
for raw_stripped_event in raw_stripped_state_events:
|
||||
stripped_state_event = parse_stripped_state_event(
|
||||
raw_stripped_event
|
||||
)
|
||||
if stripped_state_event is not None:
|
||||
stripped_state_map[
|
||||
(
|
||||
stripped_state_event.type,
|
||||
stripped_state_event.state_key,
|
||||
)
|
||||
] = stripped_state_event
|
||||
|
||||
room_id_to_stripped_state_map[room_id] = stripped_state_map
|
||||
|
||||
return room_id_to_stripped_state_map
|
||||
|
||||
async def _bulk_get_partial_current_state_content_for_rooms(
|
||||
self,
|
||||
content_type: Literal[
|
||||
# `content.type` from `EventTypes.Create``
|
||||
"room_type",
|
||||
# `content.algorithm` from `EventTypes.RoomEncryption`
|
||||
"room_encryption",
|
||||
],
|
||||
room_ids: Set[str],
|
||||
sync_room_map: Dict[str, _RoomMembershipForUser],
|
||||
to_token: StreamToken,
|
||||
room_id_to_stripped_state_map: Dict[
|
||||
str, Optional[StateMap[StrippedStateEvent]]
|
||||
],
|
||||
) -> Mapping[str, Union[Optional[str], StateSentinel]]:
|
||||
"""
|
||||
Get the given state event content for a list of rooms. First we check the
|
||||
current state of the room, then fallback to stripped state if available, then
|
||||
historical state.
|
||||
|
||||
Args:
|
||||
content_type: Which content to grab
|
||||
room_ids: Room IDs to fetch the given content field for.
|
||||
sync_room_map: Dictionary of room IDs to sort along with membership
|
||||
information in the room at the time of `to_token`.
|
||||
to_token: We filter based on the state of the room at this token
|
||||
room_id_to_stripped_state_map: This does not need to be filled in before
|
||||
calling this function. Mapping from room_id to mapping of (type, state_key)
|
||||
to stripped state event. Modified in place when we fetch new rooms so we can
|
||||
save work next time this function is called.
|
||||
|
||||
Returns:
|
||||
A mapping from room ID to the state event content if the room has
|
||||
the given state event (event_type, ""), otherwise `None`. Rooms unknown to
|
||||
this server will return `ROOM_UNKNOWN_SENTINEL`.
|
||||
"""
|
||||
room_id_to_content: Dict[str, Union[Optional[str], StateSentinel]] = {}
|
||||
|
||||
# As a bulk shortcut, use the current state if the server is particpating in the
|
||||
# room (meaning we have current state). Ideally, for leave/ban rooms, we would
|
||||
# want the state at the time of the membership instead of current state to not
|
||||
# leak anything but we consider the create/encryption stripped state events to
|
||||
# not be a secret given they are often set at the start of the room and they are
|
||||
# normally handed out on invite/knock.
|
||||
#
|
||||
# Be mindful to only use this for non-sensitive details. For example, even
|
||||
# though the room name/avatar/topic are also stripped state, they seem a lot
|
||||
# more senstive to leak the current state value of.
|
||||
#
|
||||
# Since this function is cached, we need to make a mutable copy via
|
||||
# `dict(...)`.
|
||||
event_type = ""
|
||||
event_content_field = ""
|
||||
if content_type == "room_type":
|
||||
event_type = EventTypes.Create
|
||||
event_content_field = EventContentFields.ROOM_TYPE
|
||||
room_id_to_content = dict(await self.store.bulk_get_room_type(room_ids))
|
||||
elif content_type == "room_encryption":
|
||||
event_type = EventTypes.RoomEncryption
|
||||
event_content_field = EventContentFields.ENCRYPTION_ALGORITHM
|
||||
room_id_to_content = dict(
|
||||
await self.store.bulk_get_room_encryption(room_ids)
|
||||
)
|
||||
else:
|
||||
assert_never(content_type)
|
||||
|
||||
room_ids_with_results = [
|
||||
room_id
|
||||
for room_id, content_field in room_id_to_content.items()
|
||||
if content_field is not ROOM_UNKNOWN_SENTINEL
|
||||
]
|
||||
|
||||
# We might not have current room state for remote invite/knocks if we are
|
||||
# the first person on our server to see the room. The best we can do is look
|
||||
# in the optional stripped state from the invite/knock event.
|
||||
room_ids_without_results = room_ids.difference(
|
||||
chain(
|
||||
room_ids_with_results,
|
||||
[
|
||||
room_id
|
||||
for room_id, stripped_state_map in room_id_to_stripped_state_map.items()
|
||||
if stripped_state_map is not None
|
||||
],
|
||||
)
|
||||
)
|
||||
room_id_to_stripped_state_map.update(
|
||||
await self._bulk_get_stripped_state_for_rooms_from_sync_room_map(
|
||||
room_ids_without_results, sync_room_map
|
||||
)
|
||||
)
|
||||
|
||||
# Update our `room_id_to_content` map based on the stripped state
|
||||
# (applies to invite/knock rooms)
|
||||
rooms_ids_without_stripped_state: Set[str] = set()
|
||||
for room_id in room_ids_without_results:
|
||||
stripped_state_map = room_id_to_stripped_state_map.get(
|
||||
room_id, Sentinel.UNSET_SENTINEL
|
||||
)
|
||||
assert stripped_state_map is not Sentinel.UNSET_SENTINEL, (
|
||||
f"Stripped state left unset for room {room_id}. "
|
||||
+ "Make sure you're calling `_bulk_get_stripped_state_for_rooms_from_sync_room_map(...)` "
|
||||
+ "with that room_id. (this is a problem with Synapse itself)"
|
||||
)
|
||||
|
||||
# If there is some stripped state, we assume the remote server passed *all*
|
||||
# of the potential stripped state events for the room.
|
||||
if stripped_state_map is not None:
|
||||
create_stripped_event = stripped_state_map.get((EventTypes.Create, ""))
|
||||
stripped_event = stripped_state_map.get((event_type, ""))
|
||||
# Sanity check that we at-least have the create event
|
||||
if create_stripped_event is not None:
|
||||
if stripped_event is not None:
|
||||
room_id_to_content[room_id] = stripped_event.content.get(
|
||||
event_content_field
|
||||
)
|
||||
else:
|
||||
# Didn't see the state event we're looking for in the stripped
|
||||
# state so we can assume relevant content field is `None`.
|
||||
room_id_to_content[room_id] = None
|
||||
else:
|
||||
rooms_ids_without_stripped_state.add(room_id)
|
||||
|
||||
# Last resort, we might not have current room state for rooms that the
|
||||
# server has left (no one local is in the room) but we can look at the
|
||||
# historical state.
|
||||
#
|
||||
# Update our `room_id_to_content` map based on the state at the time of
|
||||
# the membership event.
|
||||
for room_id in rooms_ids_without_stripped_state:
|
||||
# TODO: It would be nice to look this up in a bulk way (N+1 queries)
|
||||
#
|
||||
# TODO: `get_state_at(...)` doesn't take into account the "current state".
|
||||
room_state = await self.storage_controllers.state.get_state_at(
|
||||
room_id=room_id,
|
||||
stream_position=to_token.copy_and_replace(
|
||||
StreamKeyType.ROOM,
|
||||
sync_room_map[room_id].event_pos.to_room_stream_token(),
|
||||
),
|
||||
state_filter=StateFilter.from_types(
|
||||
[
|
||||
(EventTypes.Create, ""),
|
||||
(event_type, ""),
|
||||
]
|
||||
),
|
||||
# Partially-stated rooms should have all state events except for
|
||||
# remote membership events so we don't need to wait at all because
|
||||
# we only want the create event and some non-member event.
|
||||
await_full_state=False,
|
||||
)
|
||||
# We can use the create event as a canary to tell whether the server has
|
||||
# seen the room before
|
||||
create_event = room_state.get((EventTypes.Create, ""))
|
||||
state_event = room_state.get((event_type, ""))
|
||||
|
||||
if create_event is None:
|
||||
# Skip for unknown rooms
|
||||
continue
|
||||
|
||||
if state_event is not None:
|
||||
room_id_to_content[room_id] = state_event.content.get(
|
||||
event_content_field
|
||||
)
|
||||
else:
|
||||
# Didn't see the state event we're looking for in the stripped
|
||||
# state so we can assume relevant content field is `None`.
|
||||
room_id_to_content[room_id] = None
|
||||
|
||||
return room_id_to_content
|
||||
|
||||
@trace
|
||||
async def filter_rooms(
|
||||
self,
|
||||
@ -1194,6 +1473,10 @@ class SlidingSyncHandler:
|
||||
A filtered dictionary of room IDs along with membership information in the
|
||||
room at the time of `to_token`.
|
||||
"""
|
||||
room_id_to_stripped_state_map: Dict[
|
||||
str, Optional[StateMap[StrippedStateEvent]]
|
||||
] = {}
|
||||
|
||||
filtered_room_id_set = set(sync_room_map.keys())
|
||||
|
||||
# Filter for Direct-Message (DM) rooms
|
||||
@ -1213,31 +1496,34 @@ class SlidingSyncHandler:
|
||||
if not sync_room_map[room_id].is_dm
|
||||
}
|
||||
|
||||
if filters.spaces:
|
||||
if filters.spaces is not None:
|
||||
raise NotImplementedError()
|
||||
|
||||
# Filter for encrypted rooms
|
||||
if filters.is_encrypted is not None:
|
||||
room_id_to_encryption = (
|
||||
await self._bulk_get_partial_current_state_content_for_rooms(
|
||||
content_type="room_encryption",
|
||||
room_ids=filtered_room_id_set,
|
||||
to_token=to_token,
|
||||
sync_room_map=sync_room_map,
|
||||
room_id_to_stripped_state_map=room_id_to_stripped_state_map,
|
||||
)
|
||||
)
|
||||
|
||||
# Make a copy so we don't run into an error: `Set changed size during
|
||||
# iteration`, when we filter out and remove items
|
||||
for room_id in filtered_room_id_set.copy():
|
||||
state_at_to_token = await self.storage_controllers.state.get_state_at(
|
||||
room_id,
|
||||
to_token,
|
||||
state_filter=StateFilter.from_types(
|
||||
[(EventTypes.RoomEncryption, "")]
|
||||
),
|
||||
# Partially-stated rooms should have all state events except for the
|
||||
# membership events so we don't need to wait because we only care
|
||||
# about retrieving the `EventTypes.RoomEncryption` state event here.
|
||||
# Plus we don't want to block the whole sync waiting for this one
|
||||
# room.
|
||||
await_full_state=False,
|
||||
)
|
||||
is_encrypted = state_at_to_token.get((EventTypes.RoomEncryption, ""))
|
||||
encryption = room_id_to_encryption.get(room_id, ROOM_UNKNOWN_SENTINEL)
|
||||
|
||||
# Just remove rooms if we can't determine their encryption status
|
||||
if encryption is ROOM_UNKNOWN_SENTINEL:
|
||||
filtered_room_id_set.remove(room_id)
|
||||
continue
|
||||
|
||||
# If we're looking for encrypted rooms, filter out rooms that are not
|
||||
# encrypted and vice versa
|
||||
is_encrypted = encryption is not None
|
||||
if (filters.is_encrypted and not is_encrypted) or (
|
||||
not filters.is_encrypted and is_encrypted
|
||||
):
|
||||
@ -1263,15 +1549,26 @@ class SlidingSyncHandler:
|
||||
# provided in the list. `None` is a valid type for rooms which do not have a
|
||||
# room type.
|
||||
if filters.room_types is not None or filters.not_room_types is not None:
|
||||
room_to_type = await self.store.bulk_get_room_type(
|
||||
{
|
||||
room_id
|
||||
for room_id in filtered_room_id_set
|
||||
# We only know the room types for joined rooms
|
||||
if sync_room_map[room_id].membership == Membership.JOIN
|
||||
}
|
||||
room_id_to_type = (
|
||||
await self._bulk_get_partial_current_state_content_for_rooms(
|
||||
content_type="room_type",
|
||||
room_ids=filtered_room_id_set,
|
||||
to_token=to_token,
|
||||
sync_room_map=sync_room_map,
|
||||
room_id_to_stripped_state_map=room_id_to_stripped_state_map,
|
||||
)
|
||||
)
|
||||
for room_id, room_type in room_to_type.items():
|
||||
|
||||
# Make a copy so we don't run into an error: `Set changed size during
|
||||
# iteration`, when we filter out and remove items
|
||||
for room_id in filtered_room_id_set.copy():
|
||||
room_type = room_id_to_type.get(room_id, ROOM_UNKNOWN_SENTINEL)
|
||||
|
||||
# Just remove rooms if we can't determine their type
|
||||
if room_type is ROOM_UNKNOWN_SENTINEL:
|
||||
filtered_room_id_set.remove(room_id)
|
||||
continue
|
||||
|
||||
if (
|
||||
filters.room_types is not None
|
||||
and room_type not in filters.room_types
|
||||
@ -1284,13 +1581,24 @@ class SlidingSyncHandler:
|
||||
):
|
||||
filtered_room_id_set.remove(room_id)
|
||||
|
||||
if filters.room_name_like:
|
||||
if filters.room_name_like is not None:
|
||||
# TODO: The room name is a bit more sensitive to leak than the
|
||||
# create/encryption event. Maybe we should consider a better way to fetch
|
||||
# historical state before implementing this.
|
||||
#
|
||||
# room_id_to_create_content = await self._bulk_get_partial_current_state_content_for_rooms(
|
||||
# content_type="room_name",
|
||||
# room_ids=filtered_room_id_set,
|
||||
# to_token=to_token,
|
||||
# sync_room_map=sync_room_map,
|
||||
# room_id_to_stripped_state_map=room_id_to_stripped_state_map,
|
||||
# )
|
||||
raise NotImplementedError()
|
||||
|
||||
if filters.tags:
|
||||
if filters.tags is not None:
|
||||
raise NotImplementedError()
|
||||
|
||||
if filters.not_tags:
|
||||
if filters.not_tags is not None:
|
||||
raise NotImplementedError()
|
||||
|
||||
# Assemble a new sync room map but only with the `filtered_room_id_set`
|
||||
@ -1371,14 +1679,17 @@ class SlidingSyncHandler:
|
||||
in the room at the time of `to_token`.
|
||||
to_token: The point in the stream to sync up to.
|
||||
"""
|
||||
room_state_ids: StateMap[str]
|
||||
state_ids: StateMap[str]
|
||||
# People shouldn't see past their leave/ban event
|
||||
if room_membership_for_user_at_to_token.membership in (
|
||||
Membership.LEAVE,
|
||||
Membership.BAN,
|
||||
):
|
||||
# TODO: `get_state_ids_at(...)` doesn't take into account the "current state"
|
||||
room_state_ids = await self.storage_controllers.state.get_state_ids_at(
|
||||
# TODO: `get_state_ids_at(...)` doesn't take into account the "current
|
||||
# state". Maybe we need to use
|
||||
# `get_forward_extremities_for_room_at_stream_ordering(...)` to "Fetch the
|
||||
# current state at the time."
|
||||
state_ids = await self.storage_controllers.state.get_state_ids_at(
|
||||
room_id,
|
||||
stream_position=to_token.copy_and_replace(
|
||||
StreamKeyType.ROOM,
|
||||
@ -1397,7 +1708,7 @@ class SlidingSyncHandler:
|
||||
)
|
||||
# Otherwise, we can get the latest current state in the room
|
||||
else:
|
||||
room_state_ids = await self.storage_controllers.state.get_current_state_ids(
|
||||
state_ids = await self.storage_controllers.state.get_current_state_ids(
|
||||
room_id,
|
||||
state_filter,
|
||||
# Partially-stated rooms should have all state events except for
|
||||
@ -1412,7 +1723,7 @@ class SlidingSyncHandler:
|
||||
)
|
||||
# TODO: Query `current_state_delta_stream` and reverse/rewind back to the `to_token`
|
||||
|
||||
return room_state_ids
|
||||
return state_ids
|
||||
|
||||
async def get_current_state_at(
|
||||
self,
|
||||
@ -1432,17 +1743,17 @@ class SlidingSyncHandler:
|
||||
in the room at the time of `to_token`.
|
||||
to_token: The point in the stream to sync up to.
|
||||
"""
|
||||
room_state_ids = await self.get_current_state_ids_at(
|
||||
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=state_filter,
|
||||
to_token=to_token,
|
||||
)
|
||||
|
||||
event_map = await self.store.get_events(list(room_state_ids.values()))
|
||||
event_map = await self.store.get_events(list(state_ids.values()))
|
||||
|
||||
state_map = {}
|
||||
for key, event_id in room_state_ids.items():
|
||||
for key, event_id in state_ids.items():
|
||||
event = event_map.get(event_id)
|
||||
if event:
|
||||
state_map[key] = event
|
||||
|
@ -293,7 +293,9 @@ class StatsHandler:
|
||||
"history_visibility"
|
||||
)
|
||||
elif delta.event_type == EventTypes.RoomEncryption:
|
||||
room_state["encryption"] = event_content.get("algorithm")
|
||||
room_state["encryption"] = event_content.get(
|
||||
EventContentFields.ENCRYPTION_ALGORITHM
|
||||
)
|
||||
elif delta.event_type == EventTypes.Name:
|
||||
room_state["name"] = event_content.get("name")
|
||||
elif delta.event_type == EventTypes.Topic:
|
||||
|
@ -127,6 +127,8 @@ class SQLBaseStore(metaclass=ABCMeta):
|
||||
# Purge other caches based on room state.
|
||||
self._attempt_to_invalidate_cache("get_room_summary", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_partial_current_state_ids", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_room_type", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_room_encryption", (room_id,))
|
||||
|
||||
def _invalidate_state_caches_all(self, room_id: str) -> None:
|
||||
"""Invalidates caches that are based on the current state, but does
|
||||
@ -153,6 +155,8 @@ class SQLBaseStore(metaclass=ABCMeta):
|
||||
"_get_rooms_for_local_user_where_membership_is_inner", None
|
||||
)
|
||||
self._attempt_to_invalidate_cache("get_room_summary", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_room_type", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_room_encryption", (room_id,))
|
||||
|
||||
def _attempt_to_invalidate_cache(
|
||||
self, cache_name: str, key: Optional[Collection[Any]]
|
||||
|
@ -268,13 +268,23 @@ class CacheInvalidationWorkerStore(SQLBaseStore):
|
||||
self._curr_state_delta_stream_cache.entity_has_changed(data.room_id, token) # type: ignore[attr-defined]
|
||||
|
||||
if data.type == EventTypes.Member:
|
||||
self.get_rooms_for_user.invalidate((data.state_key,)) # type: ignore[attr-defined]
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_rooms_for_user", (data.state_key,)
|
||||
)
|
||||
elif data.type == EventTypes.RoomEncryption:
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_room_encryption", (data.room_id,)
|
||||
)
|
||||
elif data.type == EventTypes.Create:
|
||||
self._attempt_to_invalidate_cache("get_room_type", (data.room_id,))
|
||||
elif row.type == EventsStreamAllStateRow.TypeId:
|
||||
assert isinstance(data, EventsStreamAllStateRow)
|
||||
# Similar to the above, but the entire caches are invalidated. This is
|
||||
# unfortunate for the membership caches, but should recover quickly.
|
||||
self._curr_state_delta_stream_cache.entity_has_changed(data.room_id, token) # type: ignore[attr-defined]
|
||||
self.get_rooms_for_user.invalidate_all() # type: ignore[attr-defined]
|
||||
self._attempt_to_invalidate_cache("get_rooms_for_user", None)
|
||||
self._attempt_to_invalidate_cache("get_room_type", (data.room_id,))
|
||||
self._attempt_to_invalidate_cache("get_room_encryption", (data.room_id,))
|
||||
else:
|
||||
raise Exception("Unknown events stream row type %s" % (row.type,))
|
||||
|
||||
@ -345,6 +355,10 @@ class CacheInvalidationWorkerStore(SQLBaseStore):
|
||||
self._attempt_to_invalidate_cache(
|
||||
"get_forgotten_rooms_for_user", (state_key,)
|
||||
)
|
||||
elif etype == EventTypes.Create:
|
||||
self._attempt_to_invalidate_cache("get_room_type", (room_id,))
|
||||
elif etype == EventTypes.RoomEncryption:
|
||||
self._attempt_to_invalidate_cache("get_room_encryption", (room_id,))
|
||||
|
||||
if relates_to:
|
||||
self._attempt_to_invalidate_cache(
|
||||
@ -405,6 +419,8 @@ class CacheInvalidationWorkerStore(SQLBaseStore):
|
||||
self._attempt_to_invalidate_cache("get_thread_summary", None)
|
||||
self._attempt_to_invalidate_cache("get_thread_participated", None)
|
||||
self._attempt_to_invalidate_cache("get_threads", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_room_type", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_room_encryption", (room_id,))
|
||||
|
||||
self._attempt_to_invalidate_cache("_get_state_group_for_event", None)
|
||||
|
||||
@ -457,6 +473,8 @@ class CacheInvalidationWorkerStore(SQLBaseStore):
|
||||
self._attempt_to_invalidate_cache("get_forgotten_rooms_for_user", None)
|
||||
self._attempt_to_invalidate_cache("_get_membership_from_event_id", None)
|
||||
self._attempt_to_invalidate_cache("get_room_version_id", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_room_type", (room_id,))
|
||||
self._attempt_to_invalidate_cache("get_room_encryption", (room_id,))
|
||||
|
||||
# And delete state caches.
|
||||
|
||||
|
@ -30,6 +30,7 @@ from typing import (
|
||||
Iterable,
|
||||
List,
|
||||
Mapping,
|
||||
MutableMapping,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
@ -72,10 +73,18 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
MAX_STATE_DELTA_HOPS = 100
|
||||
|
||||
|
||||
# Freeze so it's immutable and we can use it as a cache value
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class Sentinel:
|
||||
pass
|
||||
|
||||
|
||||
ROOM_UNKNOWN_SENTINEL = Sentinel()
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
class EventMetadata:
|
||||
"""Returned by `get_metadata_for_events`"""
|
||||
@ -300,51 +309,189 @@ class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore):
|
||||
|
||||
@cached(max_entries=10000)
|
||||
async def get_room_type(self, room_id: str) -> Optional[str]:
|
||||
"""Get the room type for a given room. The server must be joined to the
|
||||
given room.
|
||||
"""
|
||||
|
||||
row = await self.db_pool.simple_select_one(
|
||||
table="room_stats_state",
|
||||
keyvalues={"room_id": room_id},
|
||||
retcols=("room_type",),
|
||||
allow_none=True,
|
||||
desc="get_room_type",
|
||||
)
|
||||
|
||||
if row is not None:
|
||||
return row[0]
|
||||
|
||||
# If we haven't updated `room_stats_state` with the room yet, query the
|
||||
# create event directly.
|
||||
create_event = await self.get_create_event_for_room(room_id)
|
||||
room_type = create_event.content.get(EventContentFields.ROOM_TYPE)
|
||||
return room_type
|
||||
raise NotImplementedError()
|
||||
|
||||
@cachedList(cached_method_name="get_room_type", list_name="room_ids")
|
||||
async def bulk_get_room_type(
|
||||
self, room_ids: Set[str]
|
||||
) -> Mapping[str, Optional[str]]:
|
||||
"""Bulk fetch room types for the given rooms, the server must be in all
|
||||
the rooms given.
|
||||
) -> Mapping[str, Union[Optional[str], Sentinel]]:
|
||||
"""
|
||||
Bulk fetch room types for the given rooms (via current state).
|
||||
|
||||
Since this function is cached, any missing values would be cached as `None`. In
|
||||
order to distinguish between an unencrypted room that has `None` encryption and
|
||||
a room that is unknown to the server where we might want to omit the value
|
||||
(which would make it cached as `None`), instead we use the sentinel value
|
||||
`ROOM_UNKNOWN_SENTINEL`.
|
||||
|
||||
Returns:
|
||||
A mapping from room ID to the room's type (`None` is a valid room type).
|
||||
Rooms unknown to this server will return `ROOM_UNKNOWN_SENTINEL`.
|
||||
"""
|
||||
|
||||
rows = await self.db_pool.simple_select_many_batch(
|
||||
table="room_stats_state",
|
||||
column="room_id",
|
||||
iterable=room_ids,
|
||||
retcols=("room_id", "room_type"),
|
||||
desc="bulk_get_room_type",
|
||||
def txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> MutableMapping[str, Union[Optional[str], Sentinel]]:
|
||||
clause, args = make_in_list_sql_clause(
|
||||
txn.database_engine, "room_id", room_ids
|
||||
)
|
||||
|
||||
# We can't rely on `room_stats_state.room_type` if the server has left the
|
||||
# room because the `room_id` will still be in the table but everything will
|
||||
# be set to `None` but `None` is a valid room type value. We join against
|
||||
# the `room_stats_current` table which keeps track of the
|
||||
# `current_state_events` count (and a proxy value `local_users_in_room`
|
||||
# which can used to assume the server is participating in the room and has
|
||||
# current state) to ensure that the data in `room_stats_state` is up-to-date
|
||||
# with the current state.
|
||||
#
|
||||
# FIXME: Use `room_stats_current.current_state_events` instead of
|
||||
# `room_stats_current.local_users_in_room` once
|
||||
# https://github.com/element-hq/synapse/issues/17457 is fixed.
|
||||
sql = f"""
|
||||
SELECT room_id, room_type
|
||||
FROM room_stats_state
|
||||
INNER JOIN room_stats_current USING (room_id)
|
||||
WHERE
|
||||
{clause}
|
||||
AND local_users_in_room > 0
|
||||
"""
|
||||
|
||||
txn.execute(sql, args)
|
||||
|
||||
room_id_to_type_map = {}
|
||||
for row in txn:
|
||||
room_id_to_type_map[row[0]] = row[1]
|
||||
|
||||
return room_id_to_type_map
|
||||
|
||||
results = await self.db_pool.runInteraction(
|
||||
"bulk_get_room_type",
|
||||
txn,
|
||||
)
|
||||
|
||||
# If we haven't updated `room_stats_state` with the room yet, query the
|
||||
# create events directly. This should happen only rarely so we don't
|
||||
# mind if we do this in a loop.
|
||||
results = dict(rows)
|
||||
for room_id in room_ids - results.keys():
|
||||
create_event = await self.get_create_event_for_room(room_id)
|
||||
room_type = create_event.content.get(EventContentFields.ROOM_TYPE)
|
||||
results[room_id] = room_type
|
||||
try:
|
||||
create_event = await self.get_create_event_for_room(room_id)
|
||||
room_type = create_event.content.get(EventContentFields.ROOM_TYPE)
|
||||
results[room_id] = room_type
|
||||
except NotFoundError:
|
||||
# We use the sentinel value to distinguish between `None` which is a
|
||||
# valid room type and a room that is unknown to the server so the value
|
||||
# is just unset.
|
||||
results[room_id] = ROOM_UNKNOWN_SENTINEL
|
||||
|
||||
return results
|
||||
|
||||
@cached(max_entries=10000)
|
||||
async def get_room_encryption(self, room_id: str) -> Optional[str]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@cachedList(cached_method_name="get_room_encryption", list_name="room_ids")
|
||||
async def bulk_get_room_encryption(
|
||||
self, room_ids: Set[str]
|
||||
) -> Mapping[str, Union[Optional[str], Sentinel]]:
|
||||
"""
|
||||
Bulk fetch room encryption for the given rooms (via current state).
|
||||
|
||||
Since this function is cached, any missing values would be cached as `None`. In
|
||||
order to distinguish between an unencrypted room that has `None` encryption and
|
||||
a room that is unknown to the server where we might want to omit the value
|
||||
(which would make it cached as `None`), instead we use the sentinel value
|
||||
`ROOM_UNKNOWN_SENTINEL`.
|
||||
|
||||
Returns:
|
||||
A mapping from room ID to the room's encryption algorithm if the room is
|
||||
encrypted, otherwise `None`. Rooms unknown to this server will return
|
||||
`ROOM_UNKNOWN_SENTINEL`.
|
||||
"""
|
||||
|
||||
def txn(
|
||||
txn: LoggingTransaction,
|
||||
) -> MutableMapping[str, Union[Optional[str], Sentinel]]:
|
||||
clause, args = make_in_list_sql_clause(
|
||||
txn.database_engine, "room_id", room_ids
|
||||
)
|
||||
|
||||
# We can't rely on `room_stats_state.encryption` if the server has left the
|
||||
# room because the `room_id` will still be in the table but everything will
|
||||
# be set to `None` but `None` is a valid encryption value. We join against
|
||||
# the `room_stats_current` table which keeps track of the
|
||||
# `current_state_events` count (and a proxy value `local_users_in_room`
|
||||
# which can used to assume the server is participating in the room and has
|
||||
# current state) to ensure that the data in `room_stats_state` is up-to-date
|
||||
# with the current state.
|
||||
#
|
||||
# FIXME: Use `room_stats_current.current_state_events` instead of
|
||||
# `room_stats_current.local_users_in_room` once
|
||||
# https://github.com/element-hq/synapse/issues/17457 is fixed.
|
||||
sql = f"""
|
||||
SELECT room_id, encryption
|
||||
FROM room_stats_state
|
||||
INNER JOIN room_stats_current USING (room_id)
|
||||
WHERE
|
||||
{clause}
|
||||
AND local_users_in_room > 0
|
||||
"""
|
||||
|
||||
txn.execute(sql, args)
|
||||
|
||||
room_id_to_encryption_map = {}
|
||||
for row in txn:
|
||||
room_id_to_encryption_map[row[0]] = row[1]
|
||||
|
||||
return room_id_to_encryption_map
|
||||
|
||||
results = await self.db_pool.runInteraction(
|
||||
"bulk_get_room_encryption",
|
||||
txn,
|
||||
)
|
||||
|
||||
# If we haven't updated `room_stats_state` with the room yet, query the state
|
||||
# directly. This should happen only rarely so we don't mind if we do this in a
|
||||
# loop.
|
||||
encryption_event_ids: List[str] = []
|
||||
for room_id in room_ids - results.keys():
|
||||
state_map = await self.get_partial_filtered_current_state_ids(
|
||||
room_id,
|
||||
state_filter=StateFilter.from_types(
|
||||
[
|
||||
(EventTypes.Create, ""),
|
||||
(EventTypes.RoomEncryption, ""),
|
||||
]
|
||||
),
|
||||
)
|
||||
# We can use the create event as a canary to tell whether the server has
|
||||
# seen the room before
|
||||
create_event_id = state_map.get((EventTypes.Create, ""))
|
||||
encryption_event_id = state_map.get((EventTypes.RoomEncryption, ""))
|
||||
|
||||
if create_event_id is None:
|
||||
# We use the sentinel value to distinguish between `None` which is a
|
||||
# valid room type and a room that is unknown to the server so the value
|
||||
# is just unset.
|
||||
results[room_id] = ROOM_UNKNOWN_SENTINEL
|
||||
continue
|
||||
|
||||
if encryption_event_id is None:
|
||||
results[room_id] = None
|
||||
else:
|
||||
encryption_event_ids.append(encryption_event_id)
|
||||
|
||||
encryption_event_map = await self.get_events(encryption_event_ids)
|
||||
|
||||
for encryption_event_id in encryption_event_ids:
|
||||
encryption_event = encryption_event_map.get(encryption_event_id)
|
||||
# If the curent state says there is an encryption event, we should have it
|
||||
# in the database.
|
||||
assert encryption_event is not None
|
||||
|
||||
results[encryption_event.room_id] = encryption_event.content.get(
|
||||
EventContentFields.ENCRYPTION_ALGORITHM
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
#
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, List, Optional
|
||||
from unittest.mock import patch
|
||||
|
||||
from parameterized import parameterized
|
||||
@ -35,7 +35,7 @@ from synapse.api.constants import (
|
||||
RoomTypes,
|
||||
)
|
||||
from synapse.api.room_versions import RoomVersions
|
||||
from synapse.events import make_event_from_dict
|
||||
from synapse.events import StrippedStateEvent, make_event_from_dict
|
||||
from synapse.events.snapshot import EventContext
|
||||
from synapse.handlers.sliding_sync import (
|
||||
RoomSyncConfig,
|
||||
@ -3093,6 +3093,78 @@ class FilterRoomsTestCase(HomeserverTestCase):
|
||||
|
||||
return room_id
|
||||
|
||||
_remote_invite_count: int = 0
|
||||
|
||||
def _create_remote_invite_room_for_user(
|
||||
self,
|
||||
invitee_user_id: str,
|
||||
unsigned_invite_room_state: Optional[List[StrippedStateEvent]],
|
||||
) -> str:
|
||||
"""
|
||||
Create a fake invite for a remote room and persist it.
|
||||
|
||||
We don't have any state for these kind of rooms and can only rely on the
|
||||
stripped state included in the unsigned portion of the invite event to identify
|
||||
the room.
|
||||
|
||||
Args:
|
||||
invitee_user_id: The person being invited
|
||||
unsigned_invite_room_state: List of stripped state events to assist the
|
||||
receiver in identifying the room.
|
||||
|
||||
Returns:
|
||||
The room ID of the remote invite room
|
||||
"""
|
||||
invite_room_id = f"!test_room{self._remote_invite_count}:remote_server"
|
||||
|
||||
invite_event_dict = {
|
||||
"room_id": invite_room_id,
|
||||
"sender": "@inviter:remote_server",
|
||||
"state_key": invitee_user_id,
|
||||
"depth": 1,
|
||||
"origin_server_ts": 1,
|
||||
"type": EventTypes.Member,
|
||||
"content": {"membership": Membership.INVITE},
|
||||
"auth_events": [],
|
||||
"prev_events": [],
|
||||
}
|
||||
if unsigned_invite_room_state is not None:
|
||||
serialized_stripped_state_events = []
|
||||
for stripped_event in unsigned_invite_room_state:
|
||||
serialized_stripped_state_events.append(
|
||||
{
|
||||
"type": stripped_event.type,
|
||||
"state_key": stripped_event.state_key,
|
||||
"sender": stripped_event.sender,
|
||||
"content": stripped_event.content,
|
||||
}
|
||||
)
|
||||
|
||||
invite_event_dict["unsigned"] = {
|
||||
"invite_room_state": serialized_stripped_state_events
|
||||
}
|
||||
|
||||
invite_event = make_event_from_dict(
|
||||
invite_event_dict,
|
||||
room_version=RoomVersions.V10,
|
||||
)
|
||||
invite_event.internal_metadata.outlier = True
|
||||
invite_event.internal_metadata.out_of_band_membership = True
|
||||
|
||||
self.get_success(
|
||||
self.store.maybe_store_room_on_outlier_membership(
|
||||
room_id=invite_room_id, room_version=invite_event.room_version
|
||||
)
|
||||
)
|
||||
context = EventContext.for_outlier(self.hs.get_storage_controllers())
|
||||
persist_controller = self.hs.get_storage_controllers().persistence
|
||||
assert persist_controller is not None
|
||||
self.get_success(persist_controller.persist_event(invite_event, context))
|
||||
|
||||
self._remote_invite_count += 1
|
||||
|
||||
return invite_room_id
|
||||
|
||||
def test_filter_dm_rooms(self) -> None:
|
||||
"""
|
||||
Test `filter.is_dm` for DM rooms
|
||||
@ -3157,7 +3229,7 @@ class FilterRoomsTestCase(HomeserverTestCase):
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
|
||||
# Create a normal room
|
||||
# Create an unencrypted room
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
|
||||
# Create an encrypted room
|
||||
@ -3165,7 +3237,7 @@ class FilterRoomsTestCase(HomeserverTestCase):
|
||||
self.helper.send_state(
|
||||
encrypted_room_id,
|
||||
EventTypes.RoomEncryption,
|
||||
{"algorithm": "m.megolm.v1.aes-sha2"},
|
||||
{EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"},
|
||||
tok=user1_tok,
|
||||
)
|
||||
|
||||
@ -3206,6 +3278,460 @@ class FilterRoomsTestCase(HomeserverTestCase):
|
||||
|
||||
self.assertEqual(falsy_filtered_room_map.keys(), {room_id})
|
||||
|
||||
def test_filter_encrypted_server_left_room(self) -> None:
|
||||
"""
|
||||
Test that we can apply a `filter.is_encrypted` against a room that everyone has left.
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
|
||||
before_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Create an unencrypted room
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
# Leave the room
|
||||
self.helper.leave(room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Create an encrypted room
|
||||
encrypted_room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
self.helper.send_state(
|
||||
encrypted_room_id,
|
||||
EventTypes.RoomEncryption,
|
||||
{EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"},
|
||||
tok=user1_tok,
|
||||
)
|
||||
# Leave the room
|
||||
self.helper.leave(encrypted_room_id, user1_id, tok=user1_tok)
|
||||
|
||||
after_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Get the rooms the user should be syncing with
|
||||
sync_room_map = self._get_sync_room_ids_for_user(
|
||||
UserID.from_string(user1_id),
|
||||
# We're using a `from_token` so that the room is considered `newly_left` and
|
||||
# appears in our list of relevant sync rooms
|
||||
from_token=before_rooms_token,
|
||||
to_token=after_rooms_token,
|
||||
)
|
||||
|
||||
# Try with `is_encrypted=True`
|
||||
truthy_filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
is_encrypted=True,
|
||||
),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(truthy_filtered_room_map.keys(), {encrypted_room_id})
|
||||
|
||||
# Try with `is_encrypted=False`
|
||||
falsy_filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
is_encrypted=False,
|
||||
),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(falsy_filtered_room_map.keys(), {room_id})
|
||||
|
||||
def test_filter_encrypted_server_left_room2(self) -> None:
|
||||
"""
|
||||
Test that we can apply a `filter.is_encrypted` against a room that everyone has
|
||||
left.
|
||||
|
||||
There is still someone local who is invited to the rooms but that doesn't affect
|
||||
whether the server is participating in the room (users need to be joined).
|
||||
"""
|
||||
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")
|
||||
|
||||
before_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Create an unencrypted room
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
# Invite user2
|
||||
self.helper.invite(room_id, targ=user2_id, tok=user1_tok)
|
||||
# User1 leaves the room
|
||||
self.helper.leave(room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Create an encrypted room
|
||||
encrypted_room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
self.helper.send_state(
|
||||
encrypted_room_id,
|
||||
EventTypes.RoomEncryption,
|
||||
{EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"},
|
||||
tok=user1_tok,
|
||||
)
|
||||
# Invite user2
|
||||
self.helper.invite(encrypted_room_id, targ=user2_id, tok=user1_tok)
|
||||
# User1 leaves the room
|
||||
self.helper.leave(encrypted_room_id, user1_id, tok=user1_tok)
|
||||
|
||||
after_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Get the rooms the user should be syncing with
|
||||
sync_room_map = self._get_sync_room_ids_for_user(
|
||||
UserID.from_string(user1_id),
|
||||
# We're using a `from_token` so that the room is considered `newly_left` and
|
||||
# appears in our list of relevant sync rooms
|
||||
from_token=before_rooms_token,
|
||||
to_token=after_rooms_token,
|
||||
)
|
||||
|
||||
# Try with `is_encrypted=True`
|
||||
truthy_filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
is_encrypted=True,
|
||||
),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(truthy_filtered_room_map.keys(), {encrypted_room_id})
|
||||
|
||||
# Try with `is_encrypted=False`
|
||||
falsy_filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
is_encrypted=False,
|
||||
),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(falsy_filtered_room_map.keys(), {room_id})
|
||||
|
||||
def test_filter_encrypted_after_we_left(self) -> None:
|
||||
"""
|
||||
Test that we can apply a `filter.is_encrypted` against a room that was encrypted
|
||||
after we left the room (make sure we don't just use the current state)
|
||||
"""
|
||||
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")
|
||||
|
||||
before_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Create an unencrypted room
|
||||
room_id = self.helper.create_room_as(user2_id, tok=user2_tok)
|
||||
# Leave the room
|
||||
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||
self.helper.leave(room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Create a room that will be encrypted
|
||||
encrypted_after_we_left_room_id = self.helper.create_room_as(
|
||||
user2_id, tok=user2_tok
|
||||
)
|
||||
# Leave the room
|
||||
self.helper.join(encrypted_after_we_left_room_id, user1_id, tok=user1_tok)
|
||||
self.helper.leave(encrypted_after_we_left_room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Encrypt the room after we've left
|
||||
self.helper.send_state(
|
||||
encrypted_after_we_left_room_id,
|
||||
EventTypes.RoomEncryption,
|
||||
{EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"},
|
||||
tok=user2_tok,
|
||||
)
|
||||
|
||||
after_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Get the rooms the user should be syncing with
|
||||
sync_room_map = self._get_sync_room_ids_for_user(
|
||||
UserID.from_string(user1_id),
|
||||
# We're using a `from_token` so that the room is considered `newly_left` and
|
||||
# appears in our list of relevant sync rooms
|
||||
from_token=before_rooms_token,
|
||||
to_token=after_rooms_token,
|
||||
)
|
||||
|
||||
# Try with `is_encrypted=True`
|
||||
truthy_filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
is_encrypted=True,
|
||||
),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# Even though we left the room before it was encrypted, we still see it because
|
||||
# someone else on our server is still participating in the room and we "leak"
|
||||
# the current state to the left user. But we consider the room encryption status
|
||||
# to not be a secret given it's often set at the start of the room and it's one
|
||||
# of the stripped state events that is normally handed out.
|
||||
self.assertEqual(
|
||||
truthy_filtered_room_map.keys(), {encrypted_after_we_left_room_id}
|
||||
)
|
||||
|
||||
# Try with `is_encrypted=False`
|
||||
falsy_filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
is_encrypted=False,
|
||||
),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# Even though we left the room before it was encrypted... (see comment above)
|
||||
self.assertEqual(falsy_filtered_room_map.keys(), {room_id})
|
||||
|
||||
def test_filter_encrypted_with_remote_invite_room_no_stripped_state(self) -> None:
|
||||
"""
|
||||
Test that we can apply a `filter.is_encrypted` filter against a remote invite
|
||||
room without any `unsigned.invite_room_state` (stripped state).
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
|
||||
# Create a remote invite room without any `unsigned.invite_room_state`
|
||||
_remote_invite_room_id = self._create_remote_invite_room_for_user(
|
||||
user1_id, None
|
||||
)
|
||||
|
||||
# Create an unencrypted room
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
|
||||
# Create an encrypted room
|
||||
encrypted_room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
self.helper.send_state(
|
||||
encrypted_room_id,
|
||||
EventTypes.RoomEncryption,
|
||||
{EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"},
|
||||
tok=user1_tok,
|
||||
)
|
||||
|
||||
after_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Get the rooms the user should be syncing with
|
||||
sync_room_map = self._get_sync_room_ids_for_user(
|
||||
UserID.from_string(user1_id),
|
||||
from_token=None,
|
||||
to_token=after_rooms_token,
|
||||
)
|
||||
|
||||
# Try with `is_encrypted=True`
|
||||
truthy_filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
is_encrypted=True,
|
||||
),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# `remote_invite_room_id` should not appear because we can't figure out whether
|
||||
# it is encrypted or not (no stripped state, `unsigned.invite_room_state`).
|
||||
self.assertEqual(truthy_filtered_room_map.keys(), {encrypted_room_id})
|
||||
|
||||
# Try with `is_encrypted=False`
|
||||
falsy_filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
is_encrypted=False,
|
||||
),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# `remote_invite_room_id` should not appear because we can't figure out whether
|
||||
# it is encrypted or not (no stripped state, `unsigned.invite_room_state`).
|
||||
self.assertEqual(falsy_filtered_room_map.keys(), {room_id})
|
||||
|
||||
def test_filter_encrypted_with_remote_invite_encrypted_room(self) -> None:
|
||||
"""
|
||||
Test that we can apply a `filter.is_encrypted` filter against a remote invite
|
||||
encrypted room with some `unsigned.invite_room_state` (stripped state).
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
|
||||
# Create a remote invite room with some `unsigned.invite_room_state`
|
||||
# indicating that the room is encrypted.
|
||||
remote_invite_room_id = self._create_remote_invite_room_for_user(
|
||||
user1_id,
|
||||
[
|
||||
StrippedStateEvent(
|
||||
type=EventTypes.Create,
|
||||
state_key="",
|
||||
sender="@inviter:remote_server",
|
||||
content={
|
||||
EventContentFields.ROOM_CREATOR: "@inviter:remote_server",
|
||||
EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier,
|
||||
},
|
||||
),
|
||||
StrippedStateEvent(
|
||||
type=EventTypes.RoomEncryption,
|
||||
state_key="",
|
||||
sender="@inviter:remote_server",
|
||||
content={
|
||||
EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# Create an unencrypted room
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
|
||||
# Create an encrypted room
|
||||
encrypted_room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
self.helper.send_state(
|
||||
encrypted_room_id,
|
||||
EventTypes.RoomEncryption,
|
||||
{EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"},
|
||||
tok=user1_tok,
|
||||
)
|
||||
|
||||
after_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Get the rooms the user should be syncing with
|
||||
sync_room_map = self._get_sync_room_ids_for_user(
|
||||
UserID.from_string(user1_id),
|
||||
from_token=None,
|
||||
to_token=after_rooms_token,
|
||||
)
|
||||
|
||||
# Try with `is_encrypted=True`
|
||||
truthy_filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
is_encrypted=True,
|
||||
),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# `remote_invite_room_id` should appear here because it is encrypted
|
||||
# according to the stripped state
|
||||
self.assertEqual(
|
||||
truthy_filtered_room_map.keys(), {encrypted_room_id, remote_invite_room_id}
|
||||
)
|
||||
|
||||
# Try with `is_encrypted=False`
|
||||
falsy_filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
is_encrypted=False,
|
||||
),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# `remote_invite_room_id` should not appear here because it is encrypted
|
||||
# according to the stripped state
|
||||
self.assertEqual(falsy_filtered_room_map.keys(), {room_id})
|
||||
|
||||
def test_filter_encrypted_with_remote_invite_unencrypted_room(self) -> None:
|
||||
"""
|
||||
Test that we can apply a `filter.is_encrypted` filter against a remote invite
|
||||
unencrypted room with some `unsigned.invite_room_state` (stripped state).
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
|
||||
# Create a remote invite room with some `unsigned.invite_room_state`
|
||||
# but don't set any room encryption event.
|
||||
remote_invite_room_id = self._create_remote_invite_room_for_user(
|
||||
user1_id,
|
||||
[
|
||||
StrippedStateEvent(
|
||||
type=EventTypes.Create,
|
||||
state_key="",
|
||||
sender="@inviter:remote_server",
|
||||
content={
|
||||
EventContentFields.ROOM_CREATOR: "@inviter:remote_server",
|
||||
EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier,
|
||||
},
|
||||
),
|
||||
# No room encryption event
|
||||
],
|
||||
)
|
||||
|
||||
# Create an unencrypted room
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
|
||||
# Create an encrypted room
|
||||
encrypted_room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
self.helper.send_state(
|
||||
encrypted_room_id,
|
||||
EventTypes.RoomEncryption,
|
||||
{EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"},
|
||||
tok=user1_tok,
|
||||
)
|
||||
|
||||
after_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Get the rooms the user should be syncing with
|
||||
sync_room_map = self._get_sync_room_ids_for_user(
|
||||
UserID.from_string(user1_id),
|
||||
from_token=None,
|
||||
to_token=after_rooms_token,
|
||||
)
|
||||
|
||||
# Try with `is_encrypted=True`
|
||||
truthy_filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
is_encrypted=True,
|
||||
),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# `remote_invite_room_id` should not appear here because it is unencrypted
|
||||
# according to the stripped state
|
||||
self.assertEqual(truthy_filtered_room_map.keys(), {encrypted_room_id})
|
||||
|
||||
# Try with `is_encrypted=False`
|
||||
falsy_filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
is_encrypted=False,
|
||||
),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# `remote_invite_room_id` should appear because it is unencrypted according to
|
||||
# the stripped state
|
||||
self.assertEqual(
|
||||
falsy_filtered_room_map.keys(), {room_id, remote_invite_room_id}
|
||||
)
|
||||
|
||||
def test_filter_invite_rooms(self) -> None:
|
||||
"""
|
||||
Test `filter.is_invite` for rooms that the user has been invited to
|
||||
@ -3461,47 +3987,159 @@ class FilterRoomsTestCase(HomeserverTestCase):
|
||||
|
||||
self.assertEqual(filtered_room_map.keys(), {space_room_id})
|
||||
|
||||
def test_filter_room_types_with_invite_remote_room(self) -> None:
|
||||
"""Test that we can apply a room type filter, even if we have an invite
|
||||
for a remote room.
|
||||
|
||||
This is a regression test.
|
||||
def test_filter_room_types_server_left_room(self) -> None:
|
||||
"""
|
||||
Test that we can apply a `filter.room_types` against a room that everyone has left.
|
||||
"""
|
||||
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
|
||||
# Create a fake remote invite and persist it.
|
||||
invite_room_id = "!some:room"
|
||||
invite_event = make_event_from_dict(
|
||||
{
|
||||
"room_id": invite_room_id,
|
||||
"sender": "@user:test.serv",
|
||||
"state_key": user1_id,
|
||||
"depth": 1,
|
||||
"origin_server_ts": 1,
|
||||
"type": EventTypes.Member,
|
||||
"content": {"membership": Membership.INVITE},
|
||||
"auth_events": [],
|
||||
"prev_events": [],
|
||||
},
|
||||
room_version=RoomVersions.V10,
|
||||
)
|
||||
invite_event.internal_metadata.outlier = True
|
||||
invite_event.internal_metadata.out_of_band_membership = True
|
||||
|
||||
self.get_success(
|
||||
self.store.maybe_store_room_on_outlier_membership(
|
||||
room_id=invite_room_id, room_version=invite_event.room_version
|
||||
)
|
||||
)
|
||||
context = EventContext.for_outlier(self.hs.get_storage_controllers())
|
||||
persist_controller = self.hs.get_storage_controllers().persistence
|
||||
assert persist_controller is not None
|
||||
self.get_success(persist_controller.persist_event(invite_event, context))
|
||||
before_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Create a normal room (no room type)
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
# Leave the room
|
||||
self.helper.leave(room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Create a space room
|
||||
space_room_id = self.helper.create_room_as(
|
||||
user1_id,
|
||||
tok=user1_tok,
|
||||
extra_content={
|
||||
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
|
||||
},
|
||||
)
|
||||
# Leave the room
|
||||
self.helper.leave(space_room_id, user1_id, tok=user1_tok)
|
||||
|
||||
after_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Get the rooms the user should be syncing with
|
||||
sync_room_map = self._get_sync_room_ids_for_user(
|
||||
UserID.from_string(user1_id),
|
||||
# We're using a `from_token` so that the room is considered `newly_left` and
|
||||
# appears in our list of relevant sync rooms
|
||||
from_token=before_rooms_token,
|
||||
to_token=after_rooms_token,
|
||||
)
|
||||
|
||||
# Try finding only normal rooms
|
||||
filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(room_types=[None]),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(filtered_room_map.keys(), {room_id})
|
||||
|
||||
# Try finding only spaces
|
||||
filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(room_types=[RoomTypes.SPACE]),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(filtered_room_map.keys(), {space_room_id})
|
||||
|
||||
def test_filter_room_types_server_left_room2(self) -> None:
|
||||
"""
|
||||
Test that we can apply a `filter.room_types` against a room that everyone has left.
|
||||
|
||||
There is still someone local who is invited to the rooms but that doesn't affect
|
||||
whether the server is participating in the room (users need to be joined).
|
||||
"""
|
||||
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")
|
||||
|
||||
before_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Create a normal room (no room type)
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
# Invite user2
|
||||
self.helper.invite(room_id, targ=user2_id, tok=user1_tok)
|
||||
# User1 leaves the room
|
||||
self.helper.leave(room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Create a space room
|
||||
space_room_id = self.helper.create_room_as(
|
||||
user1_id,
|
||||
tok=user1_tok,
|
||||
extra_content={
|
||||
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
|
||||
},
|
||||
)
|
||||
# Invite user2
|
||||
self.helper.invite(space_room_id, targ=user2_id, tok=user1_tok)
|
||||
# User1 leaves the room
|
||||
self.helper.leave(space_room_id, user1_id, tok=user1_tok)
|
||||
|
||||
after_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Get the rooms the user should be syncing with
|
||||
sync_room_map = self._get_sync_room_ids_for_user(
|
||||
UserID.from_string(user1_id),
|
||||
# We're using a `from_token` so that the room is considered `newly_left` and
|
||||
# appears in our list of relevant sync rooms
|
||||
from_token=before_rooms_token,
|
||||
to_token=after_rooms_token,
|
||||
)
|
||||
|
||||
# Try finding only normal rooms
|
||||
filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(room_types=[None]),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(filtered_room_map.keys(), {room_id})
|
||||
|
||||
# Try finding only spaces
|
||||
filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(room_types=[RoomTypes.SPACE]),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(filtered_room_map.keys(), {space_room_id})
|
||||
|
||||
def test_filter_room_types_with_remote_invite_room_no_stripped_state(self) -> None:
|
||||
"""
|
||||
Test that we can apply a `filter.room_types` filter against a remote invite
|
||||
room without any `unsigned.invite_room_state` (stripped state).
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
|
||||
# Create a remote invite room without any `unsigned.invite_room_state`
|
||||
_remote_invite_room_id = self._create_remote_invite_room_for_user(
|
||||
user1_id, None
|
||||
)
|
||||
|
||||
# Create a normal room (no room type)
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
|
||||
# Create a space room
|
||||
space_room_id = self.helper.create_room_as(
|
||||
user1_id,
|
||||
tok=user1_tok,
|
||||
extra_content={
|
||||
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
|
||||
},
|
||||
)
|
||||
|
||||
after_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
@ -3512,18 +4150,186 @@ class FilterRoomsTestCase(HomeserverTestCase):
|
||||
to_token=after_rooms_token,
|
||||
)
|
||||
|
||||
# Try finding only normal rooms
|
||||
filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
||||
room_types=[None, RoomTypes.SPACE],
|
||||
),
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(room_types=[None]),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(filtered_room_map.keys(), {room_id, invite_room_id})
|
||||
# `remote_invite_room_id` should not appear because we can't figure out what
|
||||
# room type it is (no stripped state, `unsigned.invite_room_state`)
|
||||
self.assertEqual(filtered_room_map.keys(), {room_id})
|
||||
|
||||
# Try finding only spaces
|
||||
filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(room_types=[RoomTypes.SPACE]),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# `remote_invite_room_id` should not appear because we can't figure out what
|
||||
# room type it is (no stripped state, `unsigned.invite_room_state`)
|
||||
self.assertEqual(filtered_room_map.keys(), {space_room_id})
|
||||
|
||||
def test_filter_room_types_with_remote_invite_space(self) -> None:
|
||||
"""
|
||||
Test that we can apply a `filter.room_types` filter against a remote invite
|
||||
to a space room with some `unsigned.invite_room_state` (stripped state).
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
|
||||
# Create a remote invite room with some `unsigned.invite_room_state` indicating
|
||||
# that it is a space room
|
||||
remote_invite_room_id = self._create_remote_invite_room_for_user(
|
||||
user1_id,
|
||||
[
|
||||
StrippedStateEvent(
|
||||
type=EventTypes.Create,
|
||||
state_key="",
|
||||
sender="@inviter:remote_server",
|
||||
content={
|
||||
EventContentFields.ROOM_CREATOR: "@inviter:remote_server",
|
||||
EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier,
|
||||
# Specify that it is a space room
|
||||
EventContentFields.ROOM_TYPE: RoomTypes.SPACE,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# Create a normal room (no room type)
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
|
||||
# Create a space room
|
||||
space_room_id = self.helper.create_room_as(
|
||||
user1_id,
|
||||
tok=user1_tok,
|
||||
extra_content={
|
||||
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
|
||||
},
|
||||
)
|
||||
|
||||
after_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Get the rooms the user should be syncing with
|
||||
sync_room_map = self._get_sync_room_ids_for_user(
|
||||
UserID.from_string(user1_id),
|
||||
from_token=None,
|
||||
to_token=after_rooms_token,
|
||||
)
|
||||
|
||||
# Try finding only normal rooms
|
||||
filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(room_types=[None]),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# `remote_invite_room_id` should not appear here because it is a space room
|
||||
# according to the stripped state
|
||||
self.assertEqual(filtered_room_map.keys(), {room_id})
|
||||
|
||||
# Try finding only spaces
|
||||
filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(room_types=[RoomTypes.SPACE]),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# `remote_invite_room_id` should appear here because it is a space room
|
||||
# according to the stripped state
|
||||
self.assertEqual(
|
||||
filtered_room_map.keys(), {space_room_id, remote_invite_room_id}
|
||||
)
|
||||
|
||||
def test_filter_room_types_with_remote_invite_normal_room(self) -> None:
|
||||
"""
|
||||
Test that we can apply a `filter.room_types` filter against a remote invite
|
||||
to a normal room with some `unsigned.invite_room_state` (stripped state).
|
||||
"""
|
||||
user1_id = self.register_user("user1", "pass")
|
||||
user1_tok = self.login(user1_id, "pass")
|
||||
|
||||
# Create a remote invite room with some `unsigned.invite_room_state`
|
||||
# but the create event does not specify a room type (normal room)
|
||||
remote_invite_room_id = self._create_remote_invite_room_for_user(
|
||||
user1_id,
|
||||
[
|
||||
StrippedStateEvent(
|
||||
type=EventTypes.Create,
|
||||
state_key="",
|
||||
sender="@inviter:remote_server",
|
||||
content={
|
||||
EventContentFields.ROOM_CREATOR: "@inviter:remote_server",
|
||||
EventContentFields.ROOM_VERSION: RoomVersions.V10.identifier,
|
||||
# No room type means this is a normal room
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# Create a normal room (no room type)
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||
|
||||
# Create a space room
|
||||
space_room_id = self.helper.create_room_as(
|
||||
user1_id,
|
||||
tok=user1_tok,
|
||||
extra_content={
|
||||
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
|
||||
},
|
||||
)
|
||||
|
||||
after_rooms_token = self.event_sources.get_current_token()
|
||||
|
||||
# Get the rooms the user should be syncing with
|
||||
sync_room_map = self._get_sync_room_ids_for_user(
|
||||
UserID.from_string(user1_id),
|
||||
from_token=None,
|
||||
to_token=after_rooms_token,
|
||||
)
|
||||
|
||||
# Try finding only normal rooms
|
||||
filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(room_types=[None]),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# `remote_invite_room_id` should appear here because it is a normal room
|
||||
# according to the stripped state (no room type)
|
||||
self.assertEqual(filtered_room_map.keys(), {room_id, remote_invite_room_id})
|
||||
|
||||
# Try finding only spaces
|
||||
filtered_room_map = self.get_success(
|
||||
self.sliding_sync_handler.filter_rooms(
|
||||
UserID.from_string(user1_id),
|
||||
sync_room_map,
|
||||
SlidingSyncConfig.SlidingSyncList.Filters(room_types=[RoomTypes.SPACE]),
|
||||
after_rooms_token,
|
||||
)
|
||||
)
|
||||
|
||||
# `remote_invite_room_id` should not appear here because it is a normal room
|
||||
# according to the stripped state (no room type)
|
||||
self.assertEqual(filtered_room_map.keys(), {space_room_id})
|
||||
|
||||
|
||||
class SortRoomsTestCase(HomeserverTestCase):
|
||||
|
@ -37,6 +37,7 @@ from synapse.api.constants import (
|
||||
Membership,
|
||||
ReceiptTypes,
|
||||
RelationTypes,
|
||||
RoomTypes,
|
||||
)
|
||||
from synapse.api.room_versions import RoomVersions
|
||||
from synapse.events import EventBase
|
||||
@ -1850,6 +1851,150 @@ class SlidingSyncTestCase(SlidingSyncBase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_filter_regardless_of_membership_server_left_room(self) -> None:
|
||||
"""
|
||||
Test that filters apply to rooms regardless of membership. We're also
|
||||
compounding the problem by having all of the local users leave the room causing
|
||||
our server to leave the room.
|
||||
|
||||
We want to make sure that if someone is filtering rooms, and leaves, you still
|
||||
get that final update down sync that you left.
|
||||
"""
|
||||
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")
|
||||
|
||||
# Create a normal room
|
||||
room_id = self.helper.create_room_as(user1_id, tok=user2_tok)
|
||||
self.helper.join(room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Create an encrypted space room
|
||||
space_room_id = self.helper.create_room_as(
|
||||
user2_id,
|
||||
tok=user2_tok,
|
||||
extra_content={
|
||||
"creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE}
|
||||
},
|
||||
)
|
||||
self.helper.send_state(
|
||||
space_room_id,
|
||||
EventTypes.RoomEncryption,
|
||||
{EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"},
|
||||
tok=user2_tok,
|
||||
)
|
||||
self.helper.join(space_room_id, user1_id, tok=user1_tok)
|
||||
|
||||
# Make an initial Sliding Sync request
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.sync_endpoint,
|
||||
{
|
||||
"lists": {
|
||||
"all-list": {
|
||||
"ranges": [[0, 99]],
|
||||
"required_state": [],
|
||||
"timeline_limit": 0,
|
||||
"filters": {},
|
||||
},
|
||||
"foo-list": {
|
||||
"ranges": [[0, 99]],
|
||||
"required_state": [],
|
||||
"timeline_limit": 1,
|
||||
"filters": {
|
||||
"is_encrypted": True,
|
||||
"room_types": [RoomTypes.SPACE],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
access_token=user1_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
from_token = channel.json_body["pos"]
|
||||
|
||||
# Make sure the response has the lists we requested
|
||||
self.assertListEqual(
|
||||
list(channel.json_body["lists"].keys()),
|
||||
["all-list", "foo-list"],
|
||||
channel.json_body["lists"].keys(),
|
||||
)
|
||||
|
||||
# Make sure the lists have the correct rooms
|
||||
self.assertListEqual(
|
||||
list(channel.json_body["lists"]["all-list"]["ops"]),
|
||||
[
|
||||
{
|
||||
"op": "SYNC",
|
||||
"range": [0, 99],
|
||||
"room_ids": [space_room_id, room_id],
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertListEqual(
|
||||
list(channel.json_body["lists"]["foo-list"]["ops"]),
|
||||
[
|
||||
{
|
||||
"op": "SYNC",
|
||||
"range": [0, 99],
|
||||
"room_ids": [space_room_id],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
# Everyone leaves the encrypted space room
|
||||
self.helper.leave(space_room_id, user1_id, tok=user1_tok)
|
||||
self.helper.leave(space_room_id, user2_id, tok=user2_tok)
|
||||
|
||||
# Make an incremental Sliding Sync request
|
||||
channel = self.make_request(
|
||||
"POST",
|
||||
self.sync_endpoint + f"?pos={from_token}",
|
||||
{
|
||||
"lists": {
|
||||
"all-list": {
|
||||
"ranges": [[0, 99]],
|
||||
"required_state": [],
|
||||
"timeline_limit": 0,
|
||||
"filters": {},
|
||||
},
|
||||
"foo-list": {
|
||||
"ranges": [[0, 99]],
|
||||
"required_state": [],
|
||||
"timeline_limit": 1,
|
||||
"filters": {
|
||||
"is_encrypted": True,
|
||||
"room_types": [RoomTypes.SPACE],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
access_token=user1_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
|
||||
# Make sure the lists have the correct rooms even though we `newly_left`
|
||||
self.assertListEqual(
|
||||
list(channel.json_body["lists"]["all-list"]["ops"]),
|
||||
[
|
||||
{
|
||||
"op": "SYNC",
|
||||
"range": [0, 99],
|
||||
"room_ids": [space_room_id, room_id],
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertListEqual(
|
||||
list(channel.json_body["lists"]["foo-list"]["ops"]),
|
||||
[
|
||||
{
|
||||
"op": "SYNC",
|
||||
"range": [0, 99],
|
||||
"room_ids": [space_room_id],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
def test_sort_list(self) -> None:
|
||||
"""
|
||||
Test that the `lists` are sorted by `stream_ordering`
|
||||
|
Loading…
Reference in New Issue
Block a user