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

Federation API for Space summary #9652

Merged
merged 7 commits into from
Mar 23, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 61 additions & 9 deletions synapse/federation/transport/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import functools
import logging
import re
from typing import Optional, Tuple, Type
from typing import TYPE_CHECKING, Container, Mapping, Optional, Sequence, Tuple, Type

import synapse
from synapse.api.constants import MAX_GROUP_CATEGORYID_LENGTH, MAX_GROUP_ROLEID_LENGTH
Expand All @@ -29,7 +29,7 @@
FEDERATION_V1_PREFIX,
FEDERATION_V2_PREFIX,
)
from synapse.http.server import JsonResource
from synapse.http.server import HttpServer, JsonResource
from synapse.http.servlet import (
parse_boolean_from_args,
parse_integer_from_args,
Expand All @@ -44,10 +44,14 @@
whitelisted_homeserver,
)
from synapse.server import HomeServer
from synapse.types import ThirdPartyInstanceID, get_domain_from_id
from synapse.types import JsonDict, ThirdPartyInstanceID, get_domain_from_id
from synapse.util.ratelimitutils import FederationRateLimiter
from synapse.util.stringutils import parse_and_validate_server_name
from synapse.util.versionstring import get_version_string

if TYPE_CHECKING:
import synapse.server

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -1376,6 +1380,40 @@ async def on_PUT(self, origin, content, query, group_id):
return 200, new_content


class FederationSpaceSummaryServlet(BaseFederationServlet):
PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946"
PATH = "/spaces/(?P<room_id>[^/]*)"

async def on_POST(
self,
origin: str,
content: JsonDict,
query: Mapping[bytes, Sequence[bytes]],
room_id: str,
) -> Tuple[int, JsonDict]:
suggested_only = content.get("suggested_only", False)
if not isinstance(suggested_only, bool):
raise SynapseError(
400, "'suggested_only' must be a boolean", Codes.BAD_JSON
richvdh marked this conversation as resolved.
Show resolved Hide resolved
)

exclude_rooms = content.get("exclude_rooms", [])
if not isinstance(exclude_rooms, list) or any(
not isinstance(x, str) for x in exclude_rooms
):
raise SynapseError(400, "bad value for 'exclude_rooms'", Codes.BAD_JSON)

max_rooms_per_space = content.get("max_rooms_per_space")
if max_rooms_per_space is not None and not isinstance(max_rooms_per_space, int):
raise SynapseError(
400, "bad value for 'max_rooms_per_space'", Codes.BAD_JSON
)

return 200, await self.handler.federation_space_summary(
room_id, suggested_only, max_rooms_per_space, exclude_rooms
)


class RoomComplexityServlet(BaseFederationServlet):
"""
Indicates to other servers how complex (and therefore likely
Expand Down Expand Up @@ -1474,18 +1512,24 @@ async def on_GET(self, origin, content, query, room_id):
)


def register_servlets(hs, resource, authenticator, ratelimiter, servlet_groups=None):
def register_servlets(
hs: "synapse.server.HomeServer",
richvdh marked this conversation as resolved.
Show resolved Hide resolved
resource: HttpServer,
authenticator: Authenticator,
ratelimiter: FederationRateLimiter,
servlet_groups: Optional[Container[str]] = None,
):
"""Initialize and register servlet classes.

Will by default register all servlets. For custom behaviour, pass in
a list of servlet_groups to register.

Args:
hs (synapse.server.HomeServer): homeserver
resource (JsonResource): resource class to register to
authenticator (Authenticator): authenticator to use
ratelimiter (util.ratelimitutils.FederationRateLimiter): ratelimiter to use
servlet_groups (list[str], optional): List of servlet groups to register.
hs: homeserver
resource: resource class to register to
authenticator: authenticator to use
ratelimiter: ratelimiter to use
servlet_groups: List of servlet groups to register.
Defaults to ``DEFAULT_SERVLET_GROUPS``.
"""
if not servlet_groups:
Expand All @@ -1500,6 +1544,14 @@ def register_servlets(hs, resource, authenticator, ratelimiter, servlet_groups=N
server_name=hs.hostname,
).register(resource)

if hs.config.experimental.spaces_enabled:
FederationSpaceSummaryServlet(
handler=hs.get_space_summary_handler(),
authenticator=authenticator,
ratelimiter=ratelimiter,
server_name=hs.hostname,
).register(resource)

if "openid" in servlet_groups:
for servletclass in OPENID_SERVLET_CLASSES:
servletclass(
Expand Down
83 changes: 76 additions & 7 deletions synapse/handlers/space_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async def get_space_summary(
max_rooms_per_space: Optional[int] = None,
) -> JsonDict:
"""
Implementation of the space summary API
Implementation of the space summary C-S API

Args:
requester: user id of the user making this request
Expand Down Expand Up @@ -110,9 +110,65 @@ async def get_space_summary(

return {"rooms": rooms_result, "events": events_result}

async def federation_space_summary(
self,
room_id: str,
suggested_only: bool,
max_rooms_per_space: Optional[int],
exclude_rooms: Iterable[str],
) -> JsonDict:
richvdh marked this conversation as resolved.
Show resolved Hide resolved
"""
Implementation of the space summary Federation API

Args:
room_id: room id to start the summary at

suggested_only: whether we should only return children with the "suggested"
flag set.

max_rooms_per_space: an optional limit on the number of child rooms we will
return. Unlike the C-S API, this applies to the root room (room_id).
It is clipped to MAX_ROOMS_PER_SPACE.

exclude_rooms: a list of rooms to skip over (presumably because the
calling server has already seen them).

Returns:
summary dict to return
"""
# the queue of rooms to process
room_queue = deque((room_id,))

# the set of rooms that we should not walk further. Initialise it with the
# excluded-rooms list; we will add other rooms as we process them so that
# we do not loop.
processed_rooms = set(exclude_rooms) # type: Set[str]

rooms_result = [] # type: List[JsonDict]
events_result = [] # type: List[JsonDict]

while room_queue and len(rooms_result) < MAX_ROOMS:
room_id = room_queue.popleft()
logger.debug("Processing room %s", room_id)
processed_rooms.add(room_id)

rooms, events = await self._summarize_local_room(
None, room_id, suggested_only, max_rooms_per_space
)

rooms_result.extend(rooms)
events_result.extend(events)

# add any children that we haven't already processed to the queue
for edge_event in events:
if edge_event["state_key"] not in processed_rooms:
room_queue.append(edge_event["state_key"])

return {"rooms": rooms_result, "events": events_result}

async def _summarize_local_room(
self,
requester: str,
requester: Optional[str],
room_id: str,
suggested_only: bool,
max_children: Optional[int],
Expand Down Expand Up @@ -144,12 +200,25 @@ async def _summarize_local_room(
)
return (room_entry,), events_result
richvdh marked this conversation as resolved.
Show resolved Hide resolved

async def _is_room_accessible(self, room_id: str, requester: str) -> bool:
try:
await self._auth.check_user_in_room_or_world_readable(room_id, requester)
async def _is_room_accessible(self, room_id: str, requester: Optional[str]) -> bool:
# if we have an authenticated requesting user, first check if they are in the
# room
if requester:
try:
await self._auth.check_user_in_room(room_id, requester)
return True
except AuthError:
pass

# otherwise, check if the room is peekable
hist_vis = ""
hist_vis_ev = await self._state_handler.get_current_state(
room_id, EventTypes.RoomHistoryVisibility, ""
)
if hist_vis_ev:
hist_vis = hist_vis_ev.content.get("history_visibility")
if hist_vis == HistoryVisibility.WORLD_READABLE:
return True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be clearer to indent this since it can only be reached if the event exists.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true.

except AuthError:
pass

logger.info(
"room %s is unpeekable and user %s is not a member, omitting from summary",
Expand Down