Update /hierarchy handler

If `additional_state` is requested, figure out what state should be
pulled out based on the room's `public_state` event.
This commit is contained in:
Andrew Morgan
2026-07-22 17:15:20 +01:00
parent a58db74237
commit d3dda2dd39
2 changed files with 200 additions and 2 deletions
+2
View File
@@ -187,6 +187,8 @@ class EventTypes:
RoomPolicy: Final = "m.room.policy"
MSC4507PublicState: Final = "org.matrix.msc4507.public_state"
class ToDeviceEventTypes:
RoomKeyRequest: Final = "m.room_key_request"
+198 -2
View File
@@ -103,6 +103,7 @@ class RoomSummaryHandler:
self._event_serializer = hs.get_event_client_serializer()
self._server_name = hs.hostname
self._federation_client = hs.get_federation_client()
self._msc4507_enabled = hs.config.experimental.msc4507_enabled
self._ratelimiter = Ratelimiter(
store=self._store,
clock=hs.get_clock(),
@@ -439,8 +440,11 @@ class RoomSummaryHandler:
Returns:
The JSON hierarchy dictionary.
"""
if not self._msc4507_enabled:
additional_state = None
root_room_entry = await self._summarize_local_room(
None, origin, requested_room_id, suggested_only
None, origin, requested_room_id, suggested_only, additional_state
)
if root_room_entry is None:
# Room is inaccessible to the requesting server.
@@ -461,7 +465,12 @@ class RoomSummaryHandler:
continue
room_entry = await self._summarize_local_room(
None, origin, room_id, suggested_only, include_children=False
None,
origin,
room_id,
suggested_only,
additional_state,
include_children=False,
)
# If the room is accessible, include it in the results.
#
@@ -487,6 +496,7 @@ class RoomSummaryHandler:
origin: str | None,
room_id: str,
suggested_only: bool,
additional_state: Sequence[StateEventQuery] | None = None,
include_children: bool = True,
admin_skip_room_visibility_check: bool = False,
) -> Optional["_RoomEntry"]:
@@ -503,6 +513,9 @@ class RoomSummaryHandler:
room_id: The room ID to summarize.
suggested_only: True if only suggested children should be returned.
Otherwise, all children are returned.
additional_state: State events to include in the response if the room's
public state declaration allows them. Should be `None` if the
query parameter was not present.
include_children:
Whether to include the events of any children.
admin_skip_room_visibility_check: Whether to skip checking if the room
@@ -519,6 +532,11 @@ class RoomSummaryHandler:
room_entry = await self._build_room_entry(room_id)
if additional_state is not None:
room_entry[
"org.matrix.msc4507.additional_state"
] = await self._get_public_room_state(room_id, additional_state)
# If the room is not a space return just the room information.
if room_entry.get("room_type") != RoomTypes.SPACE or not include_children:
return _RoomEntry(room_id, room_entry)
@@ -837,6 +855,79 @@ class RoomSummaryHandler:
return room_entry
async def _get_public_room_state(
self, room_id: str, requested_state: Sequence[StateEventQuery]
) -> list[JsonDict]:
"""Get requested room state that is declared public.
The room must contain an `org.matrix.msc4507.public_state` state event.
Invalid event shapes are ignored.
Args:
room_id: The room to fetch state from.
requested_state: A set of state event descriptions that someone is
asking for.
Returns:
A list of stripped state events that match the query, if any.
"""
# See which state events the room has marked as "public".
public_state_ids = await self._storage_controllers.state.get_current_state_ids(
room_id,
state_filter=StateFilter.from_types([(EventTypes.MSC4507PublicState, "")]),
)
public_state_event_id = public_state_ids.get(
(EventTypes.MSC4507PublicState, "")
)
if public_state_event_id is None:
return []
public_state_event = await self._store.get_event(public_state_event_id)
raw_public_state = public_state_event.content.get("public_state")
if not isinstance(raw_public_state, dict):
logger.debug(
"'%s' state event in room '%s' had an invalid type for "
"its 'public_state' field: %s",
EventTypes.MSC4507PublicState,
room_id,
type(raw_public_state),
)
return []
public_state = _parse_public_state(raw_public_state)
# Fetch public room state, given the list of what's public, and a query
# for it.
permitted_state = _get_permitted_public_state(requested_state, public_state)
if not permitted_state:
# None of the requested state was allowed.
return []
# Pull the allowed state events from the room.
state_ids = await self._storage_controllers.state.get_current_state_ids(
room_id, state_filter=permitted_state
)
if not state_ids:
return []
events = await self._store.get_events_as_list(state_ids.values())
# Return the stripped state versions of each state event.
# TODO: Whether we should return stripped state is currently an open question on the MSC:
# https://github.com/matrix-org/matrix-spec-proposals/pull/4507/changes#r3589665252
stripped_state: list[JsonDict] = []
for event in sorted(events, key=lambda e: (e.type, e.state_key)):
stripped_state.append(
{
"type": event.type,
"state_key": event.state_key,
"content": event.content,
"sender": event.sender,
}
)
return stripped_state
async def _get_child_events(self, room_id: str) -> Iterable[EventBase]:
"""
Get the child events for a given room.
@@ -1005,6 +1096,111 @@ class _RoomEntry:
return result
def _parse_public_state(public_state: dict) -> StateFilter:
"""Parse an MSC4507 public state declaration into a state filter.
Invalid rules for individual event types are ignored.
Args:
The content of a `org.matrix.msc4507.public_state` state event.
Returns:
A `StateFilter` built from the parsed public state event.
"""
public_types: list[tuple[str, str | None]] = []
for event_type, public_rule in public_state.items():
if not isinstance(event_type, str):
logger.debug(
"Ignoring an entry in the MSC4507 public state declaration: "
"event type must be a string, got %s",
type(event_type).__name__,
)
continue
if not isinstance(public_rule, dict):
logger.debug(
"Ignoring MSC4507 public state rule for event type '%s': "
"rule must be an object, got %s",
event_type,
type(public_rule).__name__,
)
continue
if "state_keys" not in public_rule:
public_types.append((event_type, None))
continue
state_keys = public_rule["state_keys"]
if not isinstance(state_keys, list):
logger.debug(
"Ignoring MSC4507 public state rule for event type '%s': "
"'state_keys' must be a list, got %s",
event_type,
type(state_keys).__name__,
)
continue
if not all(isinstance(state_key, str) for state_key in state_keys):
logger.debug(
"Ignoring MSC4507 public state rule for event type '%s': "
"'state_keys' must contain only strings",
event_type,
)
continue
# If multiple state_keys exist, create a filter for each one.
public_types.extend((event_type, state_key) for state_key in state_keys)
return StateFilter.from_types(public_types)
def _get_permitted_public_state(
requested_state: Sequence[StateEventQuery], public_state: StateFilter
) -> StateFilter:
"""
Given a request for certain state events from a room, and a definition of
what state events are public, return which state event types and state_keys
we're actually allowed to return (AKA the intersection between the query
and the room's public state definition).
Args:
requested_state: A request for certain state event types and state_keys
from a room.
public_state: The parsed state event types and state_keys that a room has
explicitly marked as public.
Returns:
A frozen `StateFilter` representing the intersection of the requested
state and the room's public state declaration. May be used to pull state
events from the room.
"""
requested_filter = StateFilter.from_types(
(requested.event_type, requested.state_key) for requested in requested_state
)
permitted_state: dict[str, frozenset[str] | None] = {}
# For each requested state event, check whether it may be requested based on
# `public_state`.
for event_type, requested_state_keys in requested_filter.types.items():
if event_type not in public_state.types:
continue
public_state_keys = public_state.types[event_type]
if public_state_keys is None:
# Any state key may be requested. Allow all requested.
permitted_state[event_type] = requested_state_keys
elif requested_state_keys is None:
# Some state keys are marked as public, but ANY were requested.
# Return only those that are marked as public.
permitted_state[event_type] = public_state_keys
elif permitted_keys := requested_state_keys & public_state_keys:
# If there is an intersection between the requested state keys and
# those marked as public, return those.
permitted_state[event_type] = permitted_keys
return StateFilter.freeze(permitted_state, include_others=False)
def _has_valid_via(e: EventBase) -> bool:
via = e.content.get("via")
if not via or not isinstance(via, list):