You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

969 regels
36 KiB

  1. # Copyright 2021 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import itertools
  15. import logging
  16. import re
  17. from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Set, Tuple
  18. import attr
  19. from synapse.api.constants import (
  20. EventTypes,
  21. HistoryVisibility,
  22. JoinRules,
  23. Membership,
  24. RoomTypes,
  25. )
  26. from synapse.api.errors import (
  27. Codes,
  28. NotFoundError,
  29. StoreError,
  30. SynapseError,
  31. UnstableSpecAuthError,
  32. UnsupportedRoomVersionError,
  33. )
  34. from synapse.api.ratelimiting import Ratelimiter
  35. from synapse.events import EventBase
  36. from synapse.types import JsonDict, Requester, StrCollection
  37. from synapse.util.caches.response_cache import ResponseCache
  38. if TYPE_CHECKING:
  39. from synapse.server import HomeServer
  40. logger = logging.getLogger(__name__)
  41. # number of rooms to return. We'll stop once we hit this limit.
  42. MAX_ROOMS = 50
  43. # max number of events to return per room.
  44. MAX_ROOMS_PER_SPACE = 50
  45. # max number of federation servers to hit per room
  46. MAX_SERVERS_PER_SPACE = 3
  47. @attr.s(slots=True, frozen=True, auto_attribs=True)
  48. class _PaginationKey:
  49. """The key used to find unique pagination session."""
  50. # The first three entries match the request parameters (and cannot change
  51. # during a pagination session).
  52. room_id: str
  53. suggested_only: bool
  54. max_depth: Optional[int]
  55. # The randomly generated token.
  56. token: str
  57. @attr.s(slots=True, frozen=True, auto_attribs=True)
  58. class _PaginationSession:
  59. """The information that is stored for pagination."""
  60. # The time the pagination session was created, in milliseconds.
  61. creation_time_ms: int
  62. # The queue of rooms which are still to process.
  63. room_queue: List["_RoomQueueEntry"]
  64. # A set of rooms which have been processed.
  65. processed_rooms: Set[str]
  66. class RoomSummaryHandler:
  67. # A unique key used for pagination sessions for the room hierarchy endpoint.
  68. _PAGINATION_SESSION_TYPE = "room_hierarchy_pagination"
  69. # The time a pagination session remains valid for.
  70. _PAGINATION_SESSION_VALIDITY_PERIOD_MS = 5 * 60 * 1000
  71. def __init__(self, hs: "HomeServer"):
  72. self._event_auth_handler = hs.get_event_auth_handler()
  73. self._store = hs.get_datastores().main
  74. self._storage_controllers = hs.get_storage_controllers()
  75. self._event_serializer = hs.get_event_client_serializer()
  76. self._server_name = hs.hostname
  77. self._federation_client = hs.get_federation_client()
  78. self._ratelimiter = Ratelimiter(
  79. store=self._store, clock=hs.get_clock(), rate_hz=5, burst_count=10
  80. )
  81. # If a user tries to fetch the same page multiple times in quick succession,
  82. # only process the first attempt and return its result to subsequent requests.
  83. self._pagination_response_cache: ResponseCache[
  84. Tuple[str, str, bool, Optional[int], Optional[int], Optional[str]]
  85. ] = ResponseCache(
  86. hs.get_clock(),
  87. "get_room_hierarchy",
  88. )
  89. self._msc3266_enabled = hs.config.experimental.msc3266_enabled
  90. async def get_room_hierarchy(
  91. self,
  92. requester: Requester,
  93. requested_room_id: str,
  94. suggested_only: bool = False,
  95. max_depth: Optional[int] = None,
  96. limit: Optional[int] = None,
  97. from_token: Optional[str] = None,
  98. ) -> JsonDict:
  99. """
  100. Implementation of the room hierarchy C-S API.
  101. Args:
  102. requester: The user ID of the user making this request.
  103. requested_room_id: The room ID to start the hierarchy at (the "root" room).
  104. suggested_only: Whether we should only return children with the "suggested"
  105. flag set.
  106. max_depth: The maximum depth in the tree to explore, must be a
  107. non-negative integer.
  108. 0 would correspond to just the root room, 1 would include just
  109. the root room's children, etc.
  110. limit: An optional limit on the number of rooms to return per
  111. page. Must be a positive integer.
  112. from_token: An optional pagination token.
  113. Returns:
  114. The JSON hierarchy dictionary.
  115. """
  116. await self._ratelimiter.ratelimit(requester)
  117. # If a user tries to fetch the same page multiple times in quick succession,
  118. # only process the first attempt and return its result to subsequent requests.
  119. #
  120. # This is due to the pagination process mutating internal state, attempting
  121. # to process multiple requests for the same page will result in errors.
  122. return await self._pagination_response_cache.wrap(
  123. (
  124. requester.user.to_string(),
  125. requested_room_id,
  126. suggested_only,
  127. max_depth,
  128. limit,
  129. from_token,
  130. ),
  131. self._get_room_hierarchy,
  132. requester.user.to_string(),
  133. requested_room_id,
  134. suggested_only,
  135. max_depth,
  136. limit,
  137. from_token,
  138. )
  139. async def _get_room_hierarchy(
  140. self,
  141. requester: str,
  142. requested_room_id: str,
  143. suggested_only: bool = False,
  144. max_depth: Optional[int] = None,
  145. limit: Optional[int] = None,
  146. from_token: Optional[str] = None,
  147. ) -> JsonDict:
  148. """See docstring for SpaceSummaryHandler.get_room_hierarchy."""
  149. # First of all, check that the room is accessible.
  150. if not await self._is_local_room_accessible(requested_room_id, requester):
  151. raise UnstableSpecAuthError(
  152. 403,
  153. "User %s not in room %s, and room previews are disabled"
  154. % (requester, requested_room_id),
  155. errcode=Codes.NOT_JOINED,
  156. )
  157. # If this is continuing a previous session, pull the persisted data.
  158. if from_token:
  159. try:
  160. pagination_session = await self._store.get_session(
  161. session_type=self._PAGINATION_SESSION_TYPE,
  162. session_id=from_token,
  163. )
  164. except StoreError:
  165. raise SynapseError(400, "Unknown pagination token", Codes.INVALID_PARAM)
  166. # If the requester, room ID, suggested-only, or max depth were modified
  167. # the session is invalid.
  168. if (
  169. requester != pagination_session["requester"]
  170. or requested_room_id != pagination_session["room_id"]
  171. or suggested_only != pagination_session["suggested_only"]
  172. or max_depth != pagination_session["max_depth"]
  173. ):
  174. raise SynapseError(400, "Unknown pagination token", Codes.INVALID_PARAM)
  175. # Load the previous state.
  176. room_queue = [
  177. _RoomQueueEntry(*fields) for fields in pagination_session["room_queue"]
  178. ]
  179. processed_rooms = set(pagination_session["processed_rooms"])
  180. else:
  181. # The queue of rooms to process, the next room is last on the stack.
  182. room_queue = [_RoomQueueEntry(requested_room_id, ())]
  183. # Rooms we have already processed.
  184. processed_rooms = set()
  185. rooms_result: List[JsonDict] = []
  186. # Cap the limit to a server-side maximum.
  187. if limit is None:
  188. limit = MAX_ROOMS
  189. else:
  190. limit = min(limit, MAX_ROOMS)
  191. # Iterate through the queue until we reach the limit or run out of
  192. # rooms to include.
  193. while room_queue and len(rooms_result) < limit:
  194. queue_entry = room_queue.pop()
  195. room_id = queue_entry.room_id
  196. current_depth = queue_entry.depth
  197. if room_id in processed_rooms:
  198. # already done this room
  199. continue
  200. logger.debug("Processing room %s", room_id)
  201. # A map of summaries for children rooms that might be returned over
  202. # federation. The rationale for caching these and *maybe* using them
  203. # is to prefer any information local to the homeserver before trusting
  204. # data received over federation.
  205. children_room_entries: Dict[str, JsonDict] = {}
  206. # A set of room IDs which are children that did not have information
  207. # returned over federation and are known to be inaccessible to the
  208. # current server. We should not reach out over federation to try to
  209. # summarise these rooms.
  210. inaccessible_children: Set[str] = set()
  211. # If the room is known locally, summarise it!
  212. is_in_room = await self._store.is_host_joined(room_id, self._server_name)
  213. if is_in_room:
  214. room_entry = await self._summarize_local_room(
  215. requester,
  216. None,
  217. room_id,
  218. suggested_only,
  219. )
  220. # Otherwise, attempt to use information for federation.
  221. else:
  222. # A previous call might have included information for this room.
  223. # It can be used if either:
  224. #
  225. # 1. The room is not a space.
  226. # 2. The maximum depth has been achieved (since no children
  227. # information is needed).
  228. if queue_entry.remote_room and (
  229. queue_entry.remote_room.get("room_type") != RoomTypes.SPACE
  230. or (max_depth is not None and current_depth >= max_depth)
  231. ):
  232. room_entry = _RoomEntry(
  233. queue_entry.room_id, queue_entry.remote_room
  234. )
  235. # If the above isn't true, attempt to fetch the room
  236. # information over federation.
  237. else:
  238. (
  239. room_entry,
  240. children_room_entries,
  241. inaccessible_children,
  242. ) = await self._summarize_remote_room_hierarchy(
  243. queue_entry,
  244. suggested_only,
  245. )
  246. # Ensure this room is accessible to the requester (and not just
  247. # the homeserver).
  248. if room_entry and not await self._is_remote_room_accessible(
  249. requester, queue_entry.room_id, room_entry.room
  250. ):
  251. room_entry = None
  252. # This room has been processed and should be ignored if it appears
  253. # elsewhere in the hierarchy.
  254. processed_rooms.add(room_id)
  255. # There may or may not be a room entry based on whether it is
  256. # inaccessible to the requesting user.
  257. if room_entry:
  258. # Add the room (including the stripped m.space.child events).
  259. rooms_result.append(room_entry.as_json(for_client=True))
  260. # If this room is not at the max-depth, check if there are any
  261. # children to process.
  262. if max_depth is None or current_depth < max_depth:
  263. # The children get added in reverse order so that the next
  264. # room to process, according to the ordering, is the last
  265. # item in the list.
  266. room_queue.extend(
  267. _RoomQueueEntry(
  268. ev["state_key"],
  269. ev["content"]["via"],
  270. current_depth + 1,
  271. children_room_entries.get(ev["state_key"]),
  272. )
  273. for ev in reversed(room_entry.children_state_events)
  274. if ev["type"] == EventTypes.SpaceChild
  275. and ev["state_key"] not in inaccessible_children
  276. )
  277. result: JsonDict = {"rooms": rooms_result}
  278. # If there's additional data, generate a pagination token (and persist state).
  279. if room_queue:
  280. result["next_batch"] = await self._store.create_session(
  281. session_type=self._PAGINATION_SESSION_TYPE,
  282. value={
  283. # Information which must be identical across pagination.
  284. "requester": requester,
  285. "room_id": requested_room_id,
  286. "suggested_only": suggested_only,
  287. "max_depth": max_depth,
  288. # The stored state.
  289. "room_queue": [
  290. attr.astuple(room_entry) for room_entry in room_queue
  291. ],
  292. "processed_rooms": list(processed_rooms),
  293. },
  294. expiry_ms=self._PAGINATION_SESSION_VALIDITY_PERIOD_MS,
  295. )
  296. return result
  297. async def get_federation_hierarchy(
  298. self,
  299. origin: str,
  300. requested_room_id: str,
  301. suggested_only: bool,
  302. ) -> JsonDict:
  303. """
  304. Implementation of the room hierarchy Federation API.
  305. This is similar to get_room_hierarchy, but does not recurse into the space.
  306. It also considers whether anyone on the server may be able to access the
  307. room, as opposed to whether a specific user can.
  308. Args:
  309. origin: The server requesting the spaces summary.
  310. requested_room_id: The room ID to start the hierarchy at (the "root" room).
  311. suggested_only: whether we should only return children with the "suggested"
  312. flag set.
  313. Returns:
  314. The JSON hierarchy dictionary.
  315. """
  316. root_room_entry = await self._summarize_local_room(
  317. None, origin, requested_room_id, suggested_only
  318. )
  319. if root_room_entry is None:
  320. # Room is inaccessible to the requesting server.
  321. raise SynapseError(404, "Unknown room: %s" % (requested_room_id,))
  322. children_rooms_result: List[JsonDict] = []
  323. inaccessible_children: List[str] = []
  324. # Iterate through each child and potentially add it, but not its children,
  325. # to the response.
  326. for child_room in itertools.islice(
  327. root_room_entry.children_state_events, MAX_ROOMS_PER_SPACE
  328. ):
  329. room_id = child_room.get("state_key")
  330. assert isinstance(room_id, str)
  331. # If the room is unknown, skip it.
  332. if not await self._store.is_host_joined(room_id, self._server_name):
  333. continue
  334. room_entry = await self._summarize_local_room(
  335. None, origin, room_id, suggested_only, include_children=False
  336. )
  337. # If the room is accessible, include it in the results.
  338. #
  339. # Note that only the room summary (without information on children)
  340. # is included in the summary.
  341. if room_entry:
  342. children_rooms_result.append(room_entry.room)
  343. # Otherwise, note that the requesting server shouldn't bother
  344. # trying to summarize this room - they do not have access to it.
  345. else:
  346. inaccessible_children.append(room_id)
  347. return {
  348. # Include the requested room (including the stripped children events).
  349. "room": root_room_entry.as_json(),
  350. "children": children_rooms_result,
  351. "inaccessible_children": inaccessible_children,
  352. }
  353. async def _summarize_local_room(
  354. self,
  355. requester: Optional[str],
  356. origin: Optional[str],
  357. room_id: str,
  358. suggested_only: bool,
  359. include_children: bool = True,
  360. ) -> Optional["_RoomEntry"]:
  361. """
  362. Generate a room entry and a list of event entries for a given room.
  363. Args:
  364. requester:
  365. The user requesting the summary, if it is a local request. None
  366. if this is a federation request.
  367. origin:
  368. The server requesting the summary, if it is a federation request.
  369. None if this is a local request.
  370. room_id: The room ID to summarize.
  371. suggested_only: True if only suggested children should be returned.
  372. Otherwise, all children are returned.
  373. include_children:
  374. Whether to include the events of any children.
  375. Returns:
  376. A room entry if the room should be returned. None, otherwise.
  377. """
  378. if not await self._is_local_room_accessible(room_id, requester, origin):
  379. return None
  380. room_entry = await self._build_room_entry(room_id, for_federation=bool(origin))
  381. # If the room is not a space return just the room information.
  382. if room_entry.get("room_type") != RoomTypes.SPACE or not include_children:
  383. return _RoomEntry(room_id, room_entry)
  384. # Otherwise, look for child rooms/spaces.
  385. child_events = await self._get_child_events(room_id)
  386. if suggested_only:
  387. # we only care about suggested children
  388. child_events = filter(_is_suggested_child_event, child_events)
  389. stripped_events: List[JsonDict] = [
  390. {
  391. "type": e.type,
  392. "state_key": e.state_key,
  393. "content": e.content,
  394. "sender": e.sender,
  395. "origin_server_ts": e.origin_server_ts,
  396. }
  397. for e in child_events
  398. ]
  399. return _RoomEntry(room_id, room_entry, stripped_events)
  400. async def _summarize_remote_room_hierarchy(
  401. self, room: "_RoomQueueEntry", suggested_only: bool
  402. ) -> Tuple[Optional["_RoomEntry"], Dict[str, JsonDict], Set[str]]:
  403. """
  404. Request room entries and a list of event entries for a given room by querying a remote server.
  405. Args:
  406. room: The room to summarize.
  407. suggested_only: True if only suggested children should be returned.
  408. Otherwise, all children are returned.
  409. Returns:
  410. A tuple of:
  411. The room entry.
  412. Partial room data return over federation.
  413. A set of inaccessible children room IDs.
  414. """
  415. room_id = room.room_id
  416. logger.info("Requesting summary for %s via %s", room_id, room.via)
  417. via = itertools.islice(room.via, MAX_SERVERS_PER_SPACE)
  418. try:
  419. (
  420. room_response,
  421. children_state_events,
  422. children,
  423. inaccessible_children,
  424. ) = await self._federation_client.get_room_hierarchy(
  425. via,
  426. room_id,
  427. suggested_only=suggested_only,
  428. )
  429. except Exception as e:
  430. logger.warning(
  431. "Unable to get hierarchy of %s via federation: %s",
  432. room_id,
  433. e,
  434. exc_info=logger.isEnabledFor(logging.DEBUG),
  435. )
  436. return None, {}, set()
  437. # Map the children to their room ID.
  438. children_by_room_id = {
  439. c["room_id"]: c
  440. for c in children
  441. if "room_id" in c and isinstance(c["room_id"], str)
  442. }
  443. return (
  444. _RoomEntry(room_id, room_response, children_state_events),
  445. children_by_room_id,
  446. set(inaccessible_children),
  447. )
  448. async def _is_local_room_accessible(
  449. self, room_id: str, requester: Optional[str], origin: Optional[str] = None
  450. ) -> bool:
  451. """
  452. Calculate whether the room should be shown to the requester.
  453. It should return true if:
  454. * The requesting user is joined or can join the room (per MSC3173); or
  455. * The origin server has any user that is joined or can join the room; or
  456. * The history visibility is set to world readable.
  457. Args:
  458. room_id: The room ID to check accessibility of.
  459. requester:
  460. The user making the request, if it is a local request.
  461. None if this is a federation request.
  462. origin:
  463. The server making the request, if it is a federation request.
  464. None if this is a local request.
  465. Returns:
  466. True if the room is accessible to the requesting user or server.
  467. """
  468. state_ids = await self._storage_controllers.state.get_current_state_ids(room_id)
  469. # If there's no state for the room, it isn't known.
  470. if not state_ids:
  471. # The user might have a pending invite for the room.
  472. if requester and await self._store.get_invite_for_local_user_in_room(
  473. requester, room_id
  474. ):
  475. return True
  476. logger.info("room %s is unknown, omitting from summary", room_id)
  477. return False
  478. try:
  479. room_version = await self._store.get_room_version(room_id)
  480. except UnsupportedRoomVersionError:
  481. # If a room with an unsupported room version is encountered, ignore
  482. # it to avoid breaking the entire summary response.
  483. return False
  484. # Include the room if it has join rules of public or knock.
  485. join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""))
  486. if join_rules_event_id:
  487. join_rules_event = await self._store.get_event(join_rules_event_id)
  488. join_rule = join_rules_event.content.get("join_rule")
  489. if (
  490. join_rule == JoinRules.PUBLIC
  491. or (room_version.msc2403_knocking and join_rule == JoinRules.KNOCK)
  492. or (
  493. room_version.msc3787_knock_restricted_join_rule
  494. and join_rule == JoinRules.KNOCK_RESTRICTED
  495. )
  496. ):
  497. return True
  498. # Include the room if it is peekable.
  499. hist_vis_event_id = state_ids.get((EventTypes.RoomHistoryVisibility, ""))
  500. if hist_vis_event_id:
  501. hist_vis_ev = await self._store.get_event(hist_vis_event_id)
  502. hist_vis = hist_vis_ev.content.get("history_visibility")
  503. if hist_vis == HistoryVisibility.WORLD_READABLE:
  504. return True
  505. # Otherwise we need to check information specific to the user or server.
  506. # If we have an authenticated requesting user, check if they are a member
  507. # of the room (or can join the room).
  508. if requester:
  509. member_event_id = state_ids.get((EventTypes.Member, requester), None)
  510. # If they're in the room they can see info on it.
  511. if member_event_id:
  512. member_event = await self._store.get_event(member_event_id)
  513. if member_event.membership in (Membership.JOIN, Membership.INVITE):
  514. return True
  515. # Otherwise, check if they should be allowed access via membership in a space.
  516. if await self._event_auth_handler.has_restricted_join_rules(
  517. state_ids, room_version
  518. ):
  519. allowed_rooms = (
  520. await self._event_auth_handler.get_rooms_that_allow_join(state_ids)
  521. )
  522. if await self._event_auth_handler.is_user_in_rooms(
  523. allowed_rooms, requester
  524. ):
  525. return True
  526. # If this is a request over federation, check if the host is in the room or
  527. # has a user who could join the room.
  528. elif origin:
  529. if await self._event_auth_handler.is_host_in_room(
  530. room_id, origin
  531. ) or await self._store.is_host_invited(room_id, origin):
  532. return True
  533. # Alternately, if the host has a user in any of the spaces specified
  534. # for access, then the host can see this room (and should do filtering
  535. # if the requester cannot see it).
  536. if await self._event_auth_handler.has_restricted_join_rules(
  537. state_ids, room_version
  538. ):
  539. allowed_rooms = (
  540. await self._event_auth_handler.get_rooms_that_allow_join(state_ids)
  541. )
  542. for space_id in allowed_rooms:
  543. if await self._event_auth_handler.is_host_in_room(space_id, origin):
  544. return True
  545. logger.info(
  546. "room %s is unpeekable and requester %s is not a member / not allowed to join, omitting from summary",
  547. room_id,
  548. requester or origin,
  549. )
  550. return False
  551. async def _is_remote_room_accessible(
  552. self, requester: Optional[str], room_id: str, room: JsonDict
  553. ) -> bool:
  554. """
  555. Calculate whether the room received over federation should be shown to the requester.
  556. It should return true if:
  557. * The requester is joined or can join the room (per MSC3173).
  558. * The history visibility is set to world readable.
  559. Note that the local server is not in the requested room (which is why the
  560. remote call was made in the first place), but the user could have access
  561. due to an invite, etc.
  562. Args:
  563. requester: The user requesting the summary. If not passed only world
  564. readability is checked.
  565. room_id: The room ID returned over federation.
  566. room: The summary of the room returned over federation.
  567. Returns:
  568. True if the room is accessible to the requesting user.
  569. """
  570. # The API doesn't return the room version so assume that a
  571. # join rule of knock is valid.
  572. if (
  573. room.get("join_rule")
  574. in (JoinRules.PUBLIC, JoinRules.KNOCK, JoinRules.KNOCK_RESTRICTED)
  575. or room.get("world_readable") is True
  576. ):
  577. return True
  578. elif not requester:
  579. return False
  580. # Check if the user is a member of any of the allowed rooms from the response.
  581. allowed_rooms = room.get("allowed_room_ids")
  582. if allowed_rooms and isinstance(allowed_rooms, list):
  583. if await self._event_auth_handler.is_user_in_rooms(
  584. allowed_rooms, requester
  585. ):
  586. return True
  587. # Finally, check locally if we can access the room. The user might
  588. # already be in the room (if it was a child room), or there might be a
  589. # pending invite, etc.
  590. return await self._is_local_room_accessible(room_id, requester)
  591. async def _build_room_entry(self, room_id: str, for_federation: bool) -> JsonDict:
  592. """
  593. Generate en entry summarising a single room.
  594. Args:
  595. room_id: The room ID to summarize.
  596. for_federation: True if this is a summary requested over federation
  597. (which includes additional fields).
  598. Returns:
  599. The JSON dictionary for the room.
  600. """
  601. stats = await self._store.get_room_with_stats(room_id)
  602. # currently this should be impossible because we call
  603. # _is_local_room_accessible on the room before we get here, so
  604. # there should always be an entry
  605. assert stats is not None, "unable to retrieve stats for %s" % (room_id,)
  606. entry = {
  607. "room_id": stats["room_id"],
  608. "name": stats["name"],
  609. "topic": stats["topic"],
  610. "canonical_alias": stats["canonical_alias"],
  611. "num_joined_members": stats["joined_members"],
  612. "avatar_url": stats["avatar"],
  613. "join_rule": stats["join_rules"],
  614. "world_readable": (
  615. stats["history_visibility"] == HistoryVisibility.WORLD_READABLE
  616. ),
  617. "guest_can_join": stats["guest_access"] == "can_join",
  618. "room_type": stats["room_type"],
  619. }
  620. if self._msc3266_enabled:
  621. entry["im.nheko.summary.version"] = stats["version"]
  622. entry["im.nheko.summary.encryption"] = stats["encryption"]
  623. # Federation requests need to provide additional information so the
  624. # requested server is able to filter the response appropriately.
  625. if for_federation:
  626. current_state_ids = (
  627. await self._storage_controllers.state.get_current_state_ids(room_id)
  628. )
  629. room_version = await self._store.get_room_version(room_id)
  630. if await self._event_auth_handler.has_restricted_join_rules(
  631. current_state_ids, room_version
  632. ):
  633. allowed_rooms = (
  634. await self._event_auth_handler.get_rooms_that_allow_join(
  635. current_state_ids
  636. )
  637. )
  638. if allowed_rooms:
  639. entry["allowed_room_ids"] = allowed_rooms
  640. # Filter out Nones – rather omit the field altogether
  641. room_entry = {k: v for k, v in entry.items() if v is not None}
  642. return room_entry
  643. async def _get_child_events(self, room_id: str) -> Iterable[EventBase]:
  644. """
  645. Get the child events for a given room.
  646. The returned results are sorted for stability.
  647. Args:
  648. room_id: The room id to get the children of.
  649. Returns:
  650. An iterable of sorted child events.
  651. """
  652. # look for child rooms/spaces.
  653. current_state_ids = await self._storage_controllers.state.get_current_state_ids(
  654. room_id
  655. )
  656. events = await self._store.get_events_as_list(
  657. [
  658. event_id
  659. for key, event_id in current_state_ids.items()
  660. if key[0] == EventTypes.SpaceChild
  661. ]
  662. )
  663. # filter out any events without a "via" (which implies it has been redacted),
  664. # and order to ensure we return stable results.
  665. return sorted(filter(_has_valid_via, events), key=_child_events_comparison_key)
  666. async def get_room_summary(
  667. self,
  668. requester: Optional[str],
  669. room_id: str,
  670. remote_room_hosts: Optional[List[str]] = None,
  671. ) -> JsonDict:
  672. """
  673. Implementation of the room summary C-S API from MSC3266
  674. Args:
  675. requester: user id of the user making this request, will be None
  676. for unauthenticated requests
  677. room_id: room id to summarise.
  678. remote_room_hosts: a list of homeservers to try fetching data through
  679. if we don't know it ourselves
  680. Returns:
  681. summary dict to return
  682. """
  683. is_in_room = await self._store.is_host_joined(room_id, self._server_name)
  684. if is_in_room:
  685. room_entry = await self._summarize_local_room(
  686. requester,
  687. None,
  688. room_id,
  689. # Suggested-only doesn't matter since no children are requested.
  690. suggested_only=False,
  691. include_children=False,
  692. )
  693. if not room_entry:
  694. raise NotFoundError("Room not found or is not accessible")
  695. room_summary = room_entry.room
  696. # If there was a requester, add their membership.
  697. if requester:
  698. (
  699. membership,
  700. _,
  701. ) = await self._store.get_local_current_membership_for_user_in_room(
  702. requester, room_id
  703. )
  704. room_summary["membership"] = membership or "leave"
  705. else:
  706. # Reuse the hierarchy query over federation
  707. if remote_room_hosts is None:
  708. raise SynapseError(400, "Missing via to query remote room")
  709. (
  710. room_entry,
  711. children_room_entries,
  712. inaccessible_children,
  713. ) = await self._summarize_remote_room_hierarchy(
  714. _RoomQueueEntry(room_id, remote_room_hosts),
  715. suggested_only=True,
  716. )
  717. # The results over federation might include rooms that we, as the
  718. # requesting server, are allowed to see, but the requesting user is
  719. # not permitted to see.
  720. #
  721. # Filter the returned results to only what is accessible to the user.
  722. if not room_entry or not await self._is_remote_room_accessible(
  723. requester, room_entry.room_id, room_entry.room
  724. ):
  725. raise NotFoundError("Room not found or is not accessible")
  726. room = dict(room_entry.room)
  727. room.pop("allowed_room_ids", None)
  728. # If there was a requester, add their membership.
  729. # We keep the membership in the local membership table unless the
  730. # room is purged even for remote rooms.
  731. if requester:
  732. (
  733. membership,
  734. _,
  735. ) = await self._store.get_local_current_membership_for_user_in_room(
  736. requester, room_id
  737. )
  738. room["membership"] = membership or "leave"
  739. return room
  740. return room_summary
  741. @attr.s(frozen=True, slots=True, auto_attribs=True)
  742. class _RoomQueueEntry:
  743. # The room ID of this entry.
  744. room_id: str
  745. # The server to query if the room is not known locally.
  746. via: StrCollection
  747. # The minimum number of hops necessary to get to this room (compared to the
  748. # originally requested room).
  749. depth: int = 0
  750. # The room summary for this room returned via federation. This will only be
  751. # used if the room is not known locally (and is not a space).
  752. remote_room: Optional[JsonDict] = None
  753. @attr.s(frozen=True, slots=True, auto_attribs=True)
  754. class _RoomEntry:
  755. room_id: str
  756. # The room summary for this room.
  757. room: JsonDict
  758. # An iterable of the sorted, stripped children events for children of this room.
  759. #
  760. # This may not include all children.
  761. children_state_events: Sequence[JsonDict] = ()
  762. def as_json(self, for_client: bool = False) -> JsonDict:
  763. """
  764. Returns a JSON dictionary suitable for the room hierarchy endpoint.
  765. It returns the room summary including the stripped m.space.child events
  766. as a sub-key.
  767. Args:
  768. for_client: If true, any server-server only fields are stripped from
  769. the result.
  770. """
  771. result = dict(self.room)
  772. # Before returning to the client, remove the allowed_room_ids key, if it
  773. # exists.
  774. if for_client:
  775. result.pop("allowed_room_ids", False)
  776. result["children_state"] = self.children_state_events
  777. return result
  778. def _has_valid_via(e: EventBase) -> bool:
  779. via = e.content.get("via")
  780. if not via or not isinstance(via, list):
  781. return False
  782. for v in via:
  783. if not isinstance(v, str):
  784. logger.debug("Ignoring edge event %s with invalid via entry", e.event_id)
  785. return False
  786. return True
  787. def _is_suggested_child_event(edge_event: EventBase) -> bool:
  788. suggested = edge_event.content.get("suggested")
  789. if isinstance(suggested, bool) and suggested:
  790. return True
  791. logger.debug("Ignorning not-suggested child %s", edge_event.state_key)
  792. return False
  793. # Order may only contain characters in the range of \x20 (space) to \x7E (~) inclusive.
  794. _INVALID_ORDER_CHARS_RE = re.compile(r"[^\x20-\x7E]")
  795. def _child_events_comparison_key(
  796. child: EventBase,
  797. ) -> Tuple[bool, Optional[str], int, str]:
  798. """
  799. Generate a value for comparing two child events for ordering.
  800. The rules for ordering are:
  801. 1. The 'order' key, if it is valid.
  802. 2. The 'origin_server_ts' of the 'm.space.child' event.
  803. 3. The 'room_id'.
  804. Args:
  805. child: The event for generating a comparison key.
  806. Returns:
  807. The comparison key as a tuple of:
  808. False if the ordering is valid.
  809. The 'order' field or None if it is not given or invalid.
  810. The 'origin_server_ts' field.
  811. The room ID.
  812. """
  813. order = child.content.get("order")
  814. # If order is not a string or doesn't meet the requirements, ignore it.
  815. if not isinstance(order, str):
  816. order = None
  817. elif len(order) > 50 or _INVALID_ORDER_CHARS_RE.search(order):
  818. order = None
  819. # Items without an order come last.
  820. return order is None, order, child.origin_server_ts, child.room_id