mirror of
https://mau.dev/maunium/synapse.git
synced 2024-11-12 04:52:26 +01: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
11 changed files with 1605 additions and 117 deletions
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.
|
# This is deprecated in MSC2175.
|
||||||
ROOM_CREATOR: Final = "creator"
|
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.
|
# Used in m.room.guest_access events.
|
||||||
GUEST_ACCESS: Final = "guest_access"
|
GUEST_ACCESS: Final = "guest_access"
|
||||||
|
|
||||||
|
@ -237,6 +242,9 @@ class EventContentFields:
|
||||||
# an unspecced field added to to-device messages to identify them uniquely-ish
|
# an unspecced field added to to-device messages to identify them uniquely-ish
|
||||||
TO_DEVICE_MSGID: Final = "org.matrix.msgid"
|
TO_DEVICE_MSGID: Final = "org.matrix.msgid"
|
||||||
|
|
||||||
|
# `m.room.encryption`` algorithm field
|
||||||
|
ENCRYPTION_ALGORITHM: Final = "algorithm"
|
||||||
|
|
||||||
|
|
||||||
class EventUnsignedContentFields:
|
class EventUnsignedContentFields:
|
||||||
"""Fields found inside the 'unsigned' data on events"""
|
"""Fields found inside the 'unsigned' data on events"""
|
||||||
|
|
|
@ -554,3 +554,22 @@ def relation_from_event(event: EventBase) -> Optional[_EventRelation]:
|
||||||
aggregation_key = None
|
aggregation_key = None
|
||||||
|
|
||||||
return _EventRelation(parent_id, rel_type, aggregation_key)
|
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.api.room_versions import RoomVersion
|
||||||
from synapse.types import JsonDict, Requester
|
from synapse.types import JsonDict, Requester
|
||||||
|
|
||||||
from . import EventBase, make_event_from_dict
|
from . import EventBase, StrippedStateEvent, make_event_from_dict
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.handlers.relations import BundledAggregations
|
from synapse.handlers.relations import BundledAggregations
|
||||||
|
@ -854,3 +854,30 @@ def strip_event(event: EventBase) -> JsonDict:
|
||||||
"content": event.content,
|
"content": event.content,
|
||||||
"sender": event.sender,
|
"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]
|
# [This file includes modifications made by New Vector Limited]
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
import enum
|
||||||
import logging
|
import logging
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
@ -26,23 +27,35 @@ from typing import (
|
||||||
Dict,
|
Dict,
|
||||||
Final,
|
Final,
|
||||||
List,
|
List,
|
||||||
|
Literal,
|
||||||
Mapping,
|
Mapping,
|
||||||
Optional,
|
Optional,
|
||||||
Sequence,
|
Sequence,
|
||||||
Set,
|
Set,
|
||||||
Tuple,
|
Tuple,
|
||||||
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
from immutabledict import immutabledict
|
from immutabledict import immutabledict
|
||||||
from typing_extensions import assert_never
|
from typing_extensions import assert_never
|
||||||
|
|
||||||
from synapse.api.constants import AccountDataTypes, Direction, EventTypes, Membership
|
from synapse.api.constants import (
|
||||||
from synapse.events import EventBase
|
AccountDataTypes,
|
||||||
from synapse.events.utils import strip_event
|
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.handlers.relations import BundledAggregations
|
||||||
from synapse.logging.opentracing import log_kv, start_active_span, tag_args, trace
|
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.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.databases.main.stream import CurrentStateDeltaMembership
|
||||||
from synapse.storage.roommember import MemberSummary
|
from synapse.storage.roommember import MemberSummary
|
||||||
from synapse.types import (
|
from synapse.types import (
|
||||||
|
@ -50,6 +63,7 @@ from synapse.types import (
|
||||||
JsonDict,
|
JsonDict,
|
||||||
JsonMapping,
|
JsonMapping,
|
||||||
MultiWriterStreamToken,
|
MultiWriterStreamToken,
|
||||||
|
MutableStateMap,
|
||||||
PersistedEventPosition,
|
PersistedEventPosition,
|
||||||
Requester,
|
Requester,
|
||||||
RoomStreamToken,
|
RoomStreamToken,
|
||||||
|
@ -71,6 +85,12 @@ if TYPE_CHECKING:
|
||||||
logger = logging.getLogger(__name__)
|
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.
|
# The event types that clients should consider as new activity.
|
||||||
DEFAULT_BUMP_EVENT_TYPES = {
|
DEFAULT_BUMP_EVENT_TYPES = {
|
||||||
EventTypes.Create,
|
EventTypes.Create,
|
||||||
|
@ -1172,6 +1192,265 @@ class SlidingSyncHandler:
|
||||||
|
|
||||||
# return None
|
# 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
|
@trace
|
||||||
async def filter_rooms(
|
async def filter_rooms(
|
||||||
self,
|
self,
|
||||||
|
@ -1194,6 +1473,10 @@ class SlidingSyncHandler:
|
||||||
A filtered dictionary of room IDs along with membership information in the
|
A filtered dictionary of room IDs along with membership information in the
|
||||||
room at the time of `to_token`.
|
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())
|
filtered_room_id_set = set(sync_room_map.keys())
|
||||||
|
|
||||||
# Filter for Direct-Message (DM) rooms
|
# Filter for Direct-Message (DM) rooms
|
||||||
|
@ -1213,31 +1496,34 @@ class SlidingSyncHandler:
|
||||||
if not sync_room_map[room_id].is_dm
|
if not sync_room_map[room_id].is_dm
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.spaces:
|
if filters.spaces is not None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
# Filter for encrypted rooms
|
# Filter for encrypted rooms
|
||||||
if filters.is_encrypted is not None:
|
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
|
# Make a copy so we don't run into an error: `Set changed size during
|
||||||
# iteration`, when we filter out and remove items
|
# iteration`, when we filter out and remove items
|
||||||
for room_id in filtered_room_id_set.copy():
|
for room_id in filtered_room_id_set.copy():
|
||||||
state_at_to_token = await self.storage_controllers.state.get_state_at(
|
encryption = room_id_to_encryption.get(room_id, ROOM_UNKNOWN_SENTINEL)
|
||||||
room_id,
|
|
||||||
to_token,
|
# Just remove rooms if we can't determine their encryption status
|
||||||
state_filter=StateFilter.from_types(
|
if encryption is ROOM_UNKNOWN_SENTINEL:
|
||||||
[(EventTypes.RoomEncryption, "")]
|
filtered_room_id_set.remove(room_id)
|
||||||
),
|
continue
|
||||||
# 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, ""))
|
|
||||||
|
|
||||||
# If we're looking for encrypted rooms, filter out rooms that are not
|
# If we're looking for encrypted rooms, filter out rooms that are not
|
||||||
# encrypted and vice versa
|
# encrypted and vice versa
|
||||||
|
is_encrypted = encryption is not None
|
||||||
if (filters.is_encrypted and not is_encrypted) or (
|
if (filters.is_encrypted and not is_encrypted) or (
|
||||||
not filters.is_encrypted and is_encrypted
|
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
|
# provided in the list. `None` is a valid type for rooms which do not have a
|
||||||
# room type.
|
# room type.
|
||||||
if filters.room_types is not None or filters.not_room_types is not None:
|
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_to_type = (
|
||||||
{
|
await self._bulk_get_partial_current_state_content_for_rooms(
|
||||||
room_id
|
content_type="room_type",
|
||||||
for room_id in filtered_room_id_set
|
room_ids=filtered_room_id_set,
|
||||||
# We only know the room types for joined rooms
|
to_token=to_token,
|
||||||
if sync_room_map[room_id].membership == Membership.JOIN
|
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 (
|
if (
|
||||||
filters.room_types is not None
|
filters.room_types is not None
|
||||||
and room_type not in filters.room_types
|
and room_type not in filters.room_types
|
||||||
|
@ -1284,13 +1581,24 @@ class SlidingSyncHandler:
|
||||||
):
|
):
|
||||||
filtered_room_id_set.remove(room_id)
|
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()
|
raise NotImplementedError()
|
||||||
|
|
||||||
if filters.tags:
|
if filters.tags is not None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
if filters.not_tags:
|
if filters.not_tags is not None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
# Assemble a new sync room map but only with the `filtered_room_id_set`
|
# 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`.
|
in the room at the time of `to_token`.
|
||||||
to_token: The point in the stream to sync up to.
|
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
|
# People shouldn't see past their leave/ban event
|
||||||
if room_membership_for_user_at_to_token.membership in (
|
if room_membership_for_user_at_to_token.membership in (
|
||||||
Membership.LEAVE,
|
Membership.LEAVE,
|
||||||
Membership.BAN,
|
Membership.BAN,
|
||||||
):
|
):
|
||||||
# TODO: `get_state_ids_at(...)` doesn't take into account the "current state"
|
# TODO: `get_state_ids_at(...)` doesn't take into account the "current
|
||||||
room_state_ids = await self.storage_controllers.state.get_state_ids_at(
|
# 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,
|
room_id,
|
||||||
stream_position=to_token.copy_and_replace(
|
stream_position=to_token.copy_and_replace(
|
||||||
StreamKeyType.ROOM,
|
StreamKeyType.ROOM,
|
||||||
|
@ -1397,7 +1708,7 @@ class SlidingSyncHandler:
|
||||||
)
|
)
|
||||||
# Otherwise, we can get the latest current state in the room
|
# Otherwise, we can get the latest current state in the room
|
||||||
else:
|
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,
|
room_id,
|
||||||
state_filter,
|
state_filter,
|
||||||
# Partially-stated rooms should have all state events except for
|
# 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`
|
# 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(
|
async def get_current_state_at(
|
||||||
self,
|
self,
|
||||||
|
@ -1432,17 +1743,17 @@ class SlidingSyncHandler:
|
||||||
in the room at the time of `to_token`.
|
in the room at the time of `to_token`.
|
||||||
to_token: The point in the stream to sync up to.
|
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_id=room_id,
|
||||||
room_membership_for_user_at_to_token=room_membership_for_user_at_to_token,
|
room_membership_for_user_at_to_token=room_membership_for_user_at_to_token,
|
||||||
state_filter=state_filter,
|
state_filter=state_filter,
|
||||||
to_token=to_token,
|
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 = {}
|
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)
|
event = event_map.get(event_id)
|
||||||
if event:
|
if event:
|
||||||
state_map[key] = event
|
state_map[key] = event
|
||||||
|
|
|
@ -293,7 +293,9 @@ class StatsHandler:
|
||||||
"history_visibility"
|
"history_visibility"
|
||||||
)
|
)
|
||||||
elif delta.event_type == EventTypes.RoomEncryption:
|
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:
|
elif delta.event_type == EventTypes.Name:
|
||||||
room_state["name"] = event_content.get("name")
|
room_state["name"] = event_content.get("name")
|
||||||
elif delta.event_type == EventTypes.Topic:
|
elif delta.event_type == EventTypes.Topic:
|
||||||
|
|
|
@ -127,6 +127,8 @@ class SQLBaseStore(metaclass=ABCMeta):
|
||||||
# Purge other caches based on room state.
|
# Purge other caches based on room state.
|
||||||
self._attempt_to_invalidate_cache("get_room_summary", (room_id,))
|
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_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:
|
def _invalidate_state_caches_all(self, room_id: str) -> None:
|
||||||
"""Invalidates caches that are based on the current state, but does
|
"""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
|
"_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_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(
|
def _attempt_to_invalidate_cache(
|
||||||
self, cache_name: str, key: Optional[Collection[Any]]
|
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]
|
self._curr_state_delta_stream_cache.entity_has_changed(data.room_id, token) # type: ignore[attr-defined]
|
||||||
|
|
||||||
if data.type == EventTypes.Member:
|
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:
|
elif row.type == EventsStreamAllStateRow.TypeId:
|
||||||
assert isinstance(data, EventsStreamAllStateRow)
|
assert isinstance(data, EventsStreamAllStateRow)
|
||||||
# Similar to the above, but the entire caches are invalidated. This is
|
# Similar to the above, but the entire caches are invalidated. This is
|
||||||
# unfortunate for the membership caches, but should recover quickly.
|
# 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._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:
|
else:
|
||||||
raise Exception("Unknown events stream row type %s" % (row.type,))
|
raise Exception("Unknown events stream row type %s" % (row.type,))
|
||||||
|
|
||||||
|
@ -345,6 +355,10 @@ class CacheInvalidationWorkerStore(SQLBaseStore):
|
||||||
self._attempt_to_invalidate_cache(
|
self._attempt_to_invalidate_cache(
|
||||||
"get_forgotten_rooms_for_user", (state_key,)
|
"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:
|
if relates_to:
|
||||||
self._attempt_to_invalidate_cache(
|
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_summary", None)
|
||||||
self._attempt_to_invalidate_cache("get_thread_participated", 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_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)
|
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_forgotten_rooms_for_user", None)
|
||||||
self._attempt_to_invalidate_cache("_get_membership_from_event_id", 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_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.
|
# And delete state caches.
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ from typing import (
|
||||||
Iterable,
|
Iterable,
|
||||||
List,
|
List,
|
||||||
Mapping,
|
Mapping,
|
||||||
|
MutableMapping,
|
||||||
Optional,
|
Optional,
|
||||||
Set,
|
Set,
|
||||||
Tuple,
|
Tuple,
|
||||||
|
@ -72,10 +73,18 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_T = TypeVar("_T")
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
MAX_STATE_DELTA_HOPS = 100
|
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)
|
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||||
class EventMetadata:
|
class EventMetadata:
|
||||||
"""Returned by `get_metadata_for_events`"""
|
"""Returned by `get_metadata_for_events`"""
|
||||||
|
@ -300,51 +309,189 @@ class StateGroupWorkerStore(EventsWorkerStore, SQLBaseStore):
|
||||||
|
|
||||||
@cached(max_entries=10000)
|
@cached(max_entries=10000)
|
||||||
async def get_room_type(self, room_id: str) -> Optional[str]:
|
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
|
raise NotImplementedError()
|
||||||
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
|
|
||||||
|
|
||||||
@cachedList(cached_method_name="get_room_type", list_name="room_ids")
|
@cachedList(cached_method_name="get_room_type", list_name="room_ids")
|
||||||
async def bulk_get_room_type(
|
async def bulk_get_room_type(
|
||||||
self, room_ids: Set[str]
|
self, room_ids: Set[str]
|
||||||
) -> Mapping[str, Optional[str]]:
|
) -> Mapping[str, Union[Optional[str], Sentinel]]:
|
||||||
"""Bulk fetch room types for the given rooms, the server must be in all
|
"""
|
||||||
the rooms given.
|
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(
|
def txn(
|
||||||
table="room_stats_state",
|
txn: LoggingTransaction,
|
||||||
column="room_id",
|
) -> MutableMapping[str, Union[Optional[str], Sentinel]]:
|
||||||
iterable=room_ids,
|
clause, args = make_in_list_sql_clause(
|
||||||
retcols=("room_id", "room_type"),
|
txn.database_engine, "room_id", room_ids
|
||||||
desc="bulk_get_room_type",
|
)
|
||||||
|
|
||||||
|
# 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
|
# 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
|
# create events directly. This should happen only rarely so we don't
|
||||||
# mind if we do this in a loop.
|
# mind if we do this in a loop.
|
||||||
results = dict(rows)
|
|
||||||
for room_id in room_ids - results.keys():
|
for room_id in room_ids - results.keys():
|
||||||
create_event = await self.get_create_event_for_room(room_id)
|
try:
|
||||||
room_type = create_event.content.get(EventContentFields.ROOM_TYPE)
|
create_event = await self.get_create_event_for_room(room_id)
|
||||||
results[room_id] = room_type
|
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
|
return results
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Dict, Optional
|
from typing import Dict, List, Optional
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from parameterized import parameterized
|
from parameterized import parameterized
|
||||||
|
@ -35,7 +35,7 @@ from synapse.api.constants import (
|
||||||
RoomTypes,
|
RoomTypes,
|
||||||
)
|
)
|
||||||
from synapse.api.room_versions import RoomVersions
|
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.events.snapshot import EventContext
|
||||||
from synapse.handlers.sliding_sync import (
|
from synapse.handlers.sliding_sync import (
|
||||||
RoomSyncConfig,
|
RoomSyncConfig,
|
||||||
|
@ -3093,6 +3093,78 @@ class FilterRoomsTestCase(HomeserverTestCase):
|
||||||
|
|
||||||
return room_id
|
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:
|
def test_filter_dm_rooms(self) -> None:
|
||||||
"""
|
"""
|
||||||
Test `filter.is_dm` for DM rooms
|
Test `filter.is_dm` for DM rooms
|
||||||
|
@ -3157,7 +3229,7 @@ class FilterRoomsTestCase(HomeserverTestCase):
|
||||||
user1_id = self.register_user("user1", "pass")
|
user1_id = self.register_user("user1", "pass")
|
||||||
user1_tok = self.login(user1_id, "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)
|
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
||||||
|
|
||||||
# Create an encrypted room
|
# Create an encrypted room
|
||||||
|
@ -3165,7 +3237,7 @@ class FilterRoomsTestCase(HomeserverTestCase):
|
||||||
self.helper.send_state(
|
self.helper.send_state(
|
||||||
encrypted_room_id,
|
encrypted_room_id,
|
||||||
EventTypes.RoomEncryption,
|
EventTypes.RoomEncryption,
|
||||||
{"algorithm": "m.megolm.v1.aes-sha2"},
|
{EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"},
|
||||||
tok=user1_tok,
|
tok=user1_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -3206,6 +3278,460 @@ class FilterRoomsTestCase(HomeserverTestCase):
|
||||||
|
|
||||||
self.assertEqual(falsy_filtered_room_map.keys(), {room_id})
|
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:
|
def test_filter_invite_rooms(self) -> None:
|
||||||
"""
|
"""
|
||||||
Test `filter.is_invite` for rooms that the user has been invited to
|
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})
|
self.assertEqual(filtered_room_map.keys(), {space_room_id})
|
||||||
|
|
||||||
def test_filter_room_types_with_invite_remote_room(self) -> None:
|
def test_filter_room_types_server_left_room(self) -> None:
|
||||||
"""Test that we can apply a room type filter, even if we have an invite
|
"""
|
||||||
for a remote room.
|
Test that we can apply a `filter.room_types` against a room that everyone has left.
|
||||||
|
|
||||||
This is a regression test.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user1_id = self.register_user("user1", "pass")
|
user1_id = self.register_user("user1", "pass")
|
||||||
user1_tok = self.login(user1_id, "pass")
|
user1_tok = self.login(user1_id, "pass")
|
||||||
|
|
||||||
# Create a fake remote invite and persist it.
|
before_rooms_token = self.event_sources.get_current_token()
|
||||||
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))
|
|
||||||
|
|
||||||
# Create a normal room (no room type)
|
# Create a normal room (no room type)
|
||||||
room_id = self.helper.create_room_as(user1_id, tok=user1_tok)
|
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()
|
after_rooms_token = self.event_sources.get_current_token()
|
||||||
|
|
||||||
|
@ -3512,18 +4150,186 @@ class FilterRoomsTestCase(HomeserverTestCase):
|
||||||
to_token=after_rooms_token,
|
to_token=after_rooms_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Try finding only normal rooms
|
||||||
filtered_room_map = self.get_success(
|
filtered_room_map = self.get_success(
|
||||||
self.sliding_sync_handler.filter_rooms(
|
self.sliding_sync_handler.filter_rooms(
|
||||||
UserID.from_string(user1_id),
|
UserID.from_string(user1_id),
|
||||||
sync_room_map,
|
sync_room_map,
|
||||||
SlidingSyncConfig.SlidingSyncList.Filters(
|
SlidingSyncConfig.SlidingSyncList.Filters(room_types=[None]),
|
||||||
room_types=[None, RoomTypes.SPACE],
|
|
||||||
),
|
|
||||||
after_rooms_token,
|
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):
|
class SortRoomsTestCase(HomeserverTestCase):
|
||||||
|
|
|
@ -37,6 +37,7 @@ from synapse.api.constants import (
|
||||||
Membership,
|
Membership,
|
||||||
ReceiptTypes,
|
ReceiptTypes,
|
||||||
RelationTypes,
|
RelationTypes,
|
||||||
|
RoomTypes,
|
||||||
)
|
)
|
||||||
from synapse.api.room_versions import RoomVersions
|
from synapse.api.room_versions import RoomVersions
|
||||||
from synapse.events import EventBase
|
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:
|
def test_sort_list(self) -> None:
|
||||||
"""
|
"""
|
||||||
Test that the `lists` are sorted by `stream_ordering`
|
Test that the `lists` are sorted by `stream_ordering`
|
||||||
|
|
Loading…
Reference in a new issue