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.
 
 
 
 
 
 

981 lines
34 KiB

  1. # Copyright 2019-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 logging
  15. from http import HTTPStatus
  16. from typing import TYPE_CHECKING, List, Optional, Tuple, cast
  17. from urllib import parse as urlparse
  18. from synapse.api.constants import Direction, EventTypes, JoinRules, Membership
  19. from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
  20. from synapse.api.filtering import Filter
  21. from synapse.handlers.pagination import (
  22. PURGE_ROOM_ACTION_NAME,
  23. SHUTDOWN_AND_PURGE_ROOM_ACTION_NAME,
  24. )
  25. from synapse.http.servlet import (
  26. ResolveRoomIdMixin,
  27. RestServlet,
  28. assert_params_in_dict,
  29. parse_enum,
  30. parse_integer,
  31. parse_json_object_from_request,
  32. parse_string,
  33. )
  34. from synapse.http.site import SynapseRequest
  35. from synapse.rest.admin._base import (
  36. admin_patterns,
  37. assert_requester_is_admin,
  38. assert_user_is_admin,
  39. )
  40. from synapse.storage.databases.main.room import RoomSortOrder
  41. from synapse.streams.config import PaginationConfig
  42. from synapse.types import JsonDict, RoomID, ScheduledTask, UserID, create_requester
  43. from synapse.types.state import StateFilter
  44. from synapse.util import json_decoder
  45. if TYPE_CHECKING:
  46. from synapse.api.auth import Auth
  47. from synapse.handlers.pagination import PaginationHandler
  48. from synapse.handlers.room import RoomShutdownHandler
  49. from synapse.server import HomeServer
  50. logger = logging.getLogger(__name__)
  51. class RoomRestV2Servlet(RestServlet):
  52. """Delete a room from server asynchronously with a background task.
  53. It is a combination and improvement of shutdown and purge room.
  54. Shuts down a room by removing all local users from the room.
  55. Blocking all future invites and joins to the room is optional.
  56. If desired any local aliases will be repointed to a new room
  57. created by `new_room_user_id` and kicked users will be auto-
  58. joined to the new room.
  59. If 'purge' is true, it will remove all traces of a room from the database.
  60. """
  61. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)$", "v2")
  62. def __init__(self, hs: "HomeServer"):
  63. self._auth = hs.get_auth()
  64. self._store = hs.get_datastores().main
  65. self._pagination_handler = hs.get_pagination_handler()
  66. self._third_party_rules = hs.get_module_api_callbacks().third_party_event_rules
  67. async def on_DELETE(
  68. self, request: SynapseRequest, room_id: str
  69. ) -> Tuple[int, JsonDict]:
  70. requester = await self._auth.get_user_by_req(request)
  71. await assert_user_is_admin(self._auth, requester)
  72. content = parse_json_object_from_request(request)
  73. block = content.get("block", False)
  74. if not isinstance(block, bool):
  75. raise SynapseError(
  76. HTTPStatus.BAD_REQUEST,
  77. "Param 'block' must be a boolean, if given",
  78. Codes.BAD_JSON,
  79. )
  80. purge = content.get("purge", True)
  81. if not isinstance(purge, bool):
  82. raise SynapseError(
  83. HTTPStatus.BAD_REQUEST,
  84. "Param 'purge' must be a boolean, if given",
  85. Codes.BAD_JSON,
  86. )
  87. force_purge = content.get("force_purge", False)
  88. if not isinstance(force_purge, bool):
  89. raise SynapseError(
  90. HTTPStatus.BAD_REQUEST,
  91. "Param 'force_purge' must be a boolean, if given",
  92. Codes.BAD_JSON,
  93. )
  94. if not RoomID.is_valid(room_id):
  95. raise SynapseError(
  96. HTTPStatus.BAD_REQUEST, "%s is not a legal room ID" % (room_id,)
  97. )
  98. # Check this here, as otherwise we'll only fail after the background job has been started.
  99. if not await self._third_party_rules.check_can_shutdown_room(
  100. requester.user.to_string(), room_id
  101. ):
  102. raise SynapseError(
  103. 403, "Shutdown of this room is forbidden", Codes.FORBIDDEN
  104. )
  105. delete_id = await self._pagination_handler.start_shutdown_and_purge_room(
  106. room_id=room_id,
  107. shutdown_params={
  108. "new_room_user_id": content.get("new_room_user_id"),
  109. "new_room_name": content.get("room_name"),
  110. "message": content.get("message"),
  111. "requester_user_id": requester.user.to_string(),
  112. "block": block,
  113. "purge": purge,
  114. "force_purge": force_purge,
  115. },
  116. )
  117. return HTTPStatus.OK, {"delete_id": delete_id}
  118. def _convert_delete_task_to_response(task: ScheduledTask) -> JsonDict:
  119. return {
  120. "delete_id": task.id,
  121. "status": task.status,
  122. "shutdown_room": task.result,
  123. }
  124. class DeleteRoomStatusByRoomIdRestServlet(RestServlet):
  125. """Get the status of the delete room background task."""
  126. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)/delete_status$", "v2")
  127. def __init__(self, hs: "HomeServer"):
  128. self._auth = hs.get_auth()
  129. self._pagination_handler = hs.get_pagination_handler()
  130. async def on_GET(
  131. self, request: SynapseRequest, room_id: str
  132. ) -> Tuple[int, JsonDict]:
  133. await assert_requester_is_admin(self._auth, request)
  134. if not RoomID.is_valid(room_id):
  135. raise SynapseError(
  136. HTTPStatus.BAD_REQUEST, "%s is not a legal room ID" % (room_id,)
  137. )
  138. delete_tasks = await self._pagination_handler.get_delete_tasks_by_room(room_id)
  139. if delete_tasks:
  140. return HTTPStatus.OK, {
  141. "results": [
  142. _convert_delete_task_to_response(task) for task in delete_tasks
  143. ],
  144. }
  145. else:
  146. raise NotFoundError("No delete task for room_id '%s' found" % room_id)
  147. class DeleteRoomStatusByDeleteIdRestServlet(RestServlet):
  148. """Get the status of the delete room background task."""
  149. PATTERNS = admin_patterns("/rooms/delete_status/(?P<delete_id>[^/]*)$", "v2")
  150. def __init__(self, hs: "HomeServer"):
  151. self._auth = hs.get_auth()
  152. self._pagination_handler = hs.get_pagination_handler()
  153. async def on_GET(
  154. self, request: SynapseRequest, delete_id: str
  155. ) -> Tuple[int, JsonDict]:
  156. await assert_requester_is_admin(self._auth, request)
  157. delete_task = await self._pagination_handler.get_delete_task(delete_id)
  158. if delete_task is None or (
  159. delete_task.action != PURGE_ROOM_ACTION_NAME
  160. and delete_task.action != SHUTDOWN_AND_PURGE_ROOM_ACTION_NAME
  161. ):
  162. raise NotFoundError("delete id '%s' not found" % delete_id)
  163. return HTTPStatus.OK, _convert_delete_task_to_response(delete_task)
  164. class ListRoomRestServlet(RestServlet):
  165. """
  166. List all rooms that are known to the homeserver. Results are returned
  167. in a dictionary containing room information. Supports pagination.
  168. """
  169. PATTERNS = admin_patterns("/rooms$")
  170. def __init__(self, hs: "HomeServer"):
  171. self.store = hs.get_datastores().main
  172. self.auth = hs.get_auth()
  173. self.admin_handler = hs.get_admin_handler()
  174. async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
  175. await assert_requester_is_admin(self.auth, request)
  176. # Extract query parameters
  177. start = parse_integer(request, "from", default=0)
  178. limit = parse_integer(request, "limit", default=100)
  179. order_by = parse_string(
  180. request,
  181. "order_by",
  182. default=RoomSortOrder.NAME.value,
  183. allowed_values=[sort_order.value for sort_order in RoomSortOrder],
  184. )
  185. search_term = parse_string(request, "search_term", encoding="utf-8")
  186. if search_term == "":
  187. raise SynapseError(
  188. HTTPStatus.BAD_REQUEST,
  189. "search_term cannot be an empty string",
  190. errcode=Codes.INVALID_PARAM,
  191. )
  192. direction = parse_enum(request, "dir", Direction, default=Direction.FORWARDS)
  193. reverse_order = True if direction == Direction.BACKWARDS else False
  194. # Return list of rooms according to parameters
  195. rooms, total_rooms = await self.store.get_rooms_paginate(
  196. start, limit, order_by, reverse_order, search_term
  197. )
  198. response = {
  199. # next_token should be opaque, so return a value the client can parse
  200. "offset": start,
  201. "rooms": rooms,
  202. "total_rooms": total_rooms,
  203. }
  204. # Are there more rooms to paginate through after this?
  205. if (start + limit) < total_rooms:
  206. # There are. Calculate where the query should start from next time
  207. # to get the next part of the list
  208. response["next_batch"] = start + limit
  209. # Is it possible to paginate backwards? Check if we currently have an
  210. # offset
  211. if start > 0:
  212. if start > limit:
  213. # Going back one iteration won't take us to the start.
  214. # Calculate new offset
  215. response["prev_batch"] = start - limit
  216. else:
  217. response["prev_batch"] = 0
  218. return HTTPStatus.OK, response
  219. class RoomRestServlet(RestServlet):
  220. """Manage a room.
  221. On GET : Get details of a room.
  222. On DELETE : Delete a room from server.
  223. It is a combination and improvement of shutdown and purge room.
  224. Shuts down a room by removing all local users from the room.
  225. Blocking all future invites and joins to the room is optional.
  226. If desired any local aliases will be repointed to a new room
  227. created by `new_room_user_id` and kicked users will be auto-
  228. joined to the new room.
  229. If 'purge' is true, it will remove all traces of a room from the database.
  230. TODO: Add on_POST to allow room creation without joining the room
  231. """
  232. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)$")
  233. def __init__(self, hs: "HomeServer"):
  234. self.auth = hs.get_auth()
  235. self.store = hs.get_datastores().main
  236. self.room_shutdown_handler = hs.get_room_shutdown_handler()
  237. self.pagination_handler = hs.get_pagination_handler()
  238. async def on_GET(
  239. self, request: SynapseRequest, room_id: str
  240. ) -> Tuple[int, JsonDict]:
  241. await assert_requester_is_admin(self.auth, request)
  242. ret = await self.store.get_room_with_stats(room_id)
  243. if not ret:
  244. raise NotFoundError("Room not found")
  245. members = await self.store.get_users_in_room(room_id)
  246. ret["joined_local_devices"] = await self.store.count_devices_by_users(members)
  247. ret["forgotten"] = await self.store.is_locally_forgotten_room(room_id)
  248. return HTTPStatus.OK, ret
  249. async def on_DELETE(
  250. self, request: SynapseRequest, room_id: str
  251. ) -> Tuple[int, JsonDict]:
  252. return await self._delete_room(
  253. request,
  254. room_id,
  255. self.auth,
  256. self.room_shutdown_handler,
  257. self.pagination_handler,
  258. )
  259. async def _delete_room(
  260. self,
  261. request: SynapseRequest,
  262. room_id: str,
  263. auth: "Auth",
  264. room_shutdown_handler: "RoomShutdownHandler",
  265. pagination_handler: "PaginationHandler",
  266. ) -> Tuple[int, JsonDict]:
  267. requester = await auth.get_user_by_req(request)
  268. await assert_user_is_admin(auth, requester)
  269. content = parse_json_object_from_request(request)
  270. block = content.get("block", False)
  271. if not isinstance(block, bool):
  272. raise SynapseError(
  273. HTTPStatus.BAD_REQUEST,
  274. "Param 'block' must be a boolean, if given",
  275. Codes.BAD_JSON,
  276. )
  277. purge = content.get("purge", True)
  278. if not isinstance(purge, bool):
  279. raise SynapseError(
  280. HTTPStatus.BAD_REQUEST,
  281. "Param 'purge' must be a boolean, if given",
  282. Codes.BAD_JSON,
  283. )
  284. force_purge = content.get("force_purge", False)
  285. if not isinstance(force_purge, bool):
  286. raise SynapseError(
  287. HTTPStatus.BAD_REQUEST,
  288. "Param 'force_purge' must be a boolean, if given",
  289. Codes.BAD_JSON,
  290. )
  291. ret = await room_shutdown_handler.shutdown_room(
  292. room_id=room_id,
  293. params={
  294. "new_room_user_id": content.get("new_room_user_id"),
  295. "new_room_name": content.get("room_name"),
  296. "message": content.get("message"),
  297. "requester_user_id": requester.user.to_string(),
  298. "block": block,
  299. "purge": purge,
  300. "force_purge": force_purge,
  301. },
  302. )
  303. # Purge room
  304. if purge:
  305. try:
  306. await pagination_handler.purge_room(room_id, force=force_purge)
  307. except NotFoundError:
  308. if block:
  309. # We can block unknown rooms with this endpoint, in which case
  310. # a failed purge is expected.
  311. pass
  312. else:
  313. # But otherwise, we expect this purge to have succeeded.
  314. raise
  315. # Cast safety: cast away the knowledge that this is a TypedDict.
  316. # See https://github.com/python/mypy/issues/4976#issuecomment-579883622
  317. # for some discussion on why this is necessary. Either way,
  318. # `ret` is an opaque dictionary blob as far as the rest of the app cares.
  319. return HTTPStatus.OK, cast(JsonDict, ret)
  320. class RoomMembersRestServlet(RestServlet):
  321. """
  322. Get members list of a room.
  323. """
  324. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)/members$")
  325. def __init__(self, hs: "HomeServer"):
  326. self.auth = hs.get_auth()
  327. self.store = hs.get_datastores().main
  328. async def on_GET(
  329. self, request: SynapseRequest, room_id: str
  330. ) -> Tuple[int, JsonDict]:
  331. await assert_requester_is_admin(self.auth, request)
  332. ret = await self.store.get_room(room_id)
  333. if not ret:
  334. raise NotFoundError("Room not found")
  335. members = await self.store.get_users_in_room(room_id)
  336. ret = {"members": members, "total": len(members)}
  337. return HTTPStatus.OK, ret
  338. class RoomStateRestServlet(RestServlet):
  339. """
  340. Get full state within a room.
  341. """
  342. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)/state$")
  343. def __init__(self, hs: "HomeServer"):
  344. self.auth = hs.get_auth()
  345. self.store = hs.get_datastores().main
  346. self._storage_controllers = hs.get_storage_controllers()
  347. self.clock = hs.get_clock()
  348. self._event_serializer = hs.get_event_client_serializer()
  349. async def on_GET(
  350. self, request: SynapseRequest, room_id: str
  351. ) -> Tuple[int, JsonDict]:
  352. await assert_requester_is_admin(self.auth, request)
  353. ret = await self.store.get_room(room_id)
  354. if not ret:
  355. raise NotFoundError("Room not found")
  356. event_ids = await self._storage_controllers.state.get_current_state_ids(room_id)
  357. events = await self.store.get_events(event_ids.values())
  358. now = self.clock.time_msec()
  359. room_state = await self._event_serializer.serialize_events(events.values(), now)
  360. ret = {"state": room_state}
  361. return HTTPStatus.OK, ret
  362. class JoinRoomAliasServlet(ResolveRoomIdMixin, RestServlet):
  363. PATTERNS = admin_patterns("/join/(?P<room_identifier>[^/]*)$")
  364. def __init__(self, hs: "HomeServer"):
  365. super().__init__(hs)
  366. self.auth = hs.get_auth()
  367. self.admin_handler = hs.get_admin_handler()
  368. self.store = hs.get_datastores().main
  369. self._storage_controllers = hs.get_storage_controllers()
  370. self.is_mine = hs.is_mine
  371. async def on_POST(
  372. self, request: SynapseRequest, room_identifier: str
  373. ) -> Tuple[int, JsonDict]:
  374. # This will always be set by the time Twisted calls us.
  375. assert request.args is not None
  376. requester = await self.auth.get_user_by_req(request)
  377. await assert_user_is_admin(self.auth, requester)
  378. content = parse_json_object_from_request(request)
  379. assert_params_in_dict(content, ["user_id"])
  380. target_user = UserID.from_string(content["user_id"])
  381. if not self.is_mine(target_user):
  382. raise SynapseError(
  383. HTTPStatus.BAD_REQUEST,
  384. "This endpoint can only be used with local users",
  385. )
  386. if not await self.admin_handler.get_user(target_user):
  387. raise NotFoundError("User not found")
  388. # Get the room ID from the identifier.
  389. try:
  390. remote_room_hosts: Optional[List[str]] = [
  391. x.decode("ascii") for x in request.args[b"server_name"]
  392. ]
  393. except Exception:
  394. remote_room_hosts = None
  395. room_id, remote_room_hosts = await self.resolve_room_id(
  396. room_identifier, remote_room_hosts
  397. )
  398. fake_requester = create_requester(
  399. target_user, authenticated_entity=requester.authenticated_entity
  400. )
  401. # send invite if room has "JoinRules.INVITE"
  402. join_rules_event = (
  403. await self._storage_controllers.state.get_current_state_event(
  404. room_id, EventTypes.JoinRules, ""
  405. )
  406. )
  407. if join_rules_event:
  408. if not (join_rules_event.content.get("join_rule") == JoinRules.PUBLIC):
  409. # update_membership with an action of "invite" can raise a
  410. # ShadowBanError. This is not handled since it is assumed that
  411. # an admin isn't going to call this API with a shadow-banned user.
  412. await self.room_member_handler.update_membership(
  413. requester=requester,
  414. target=fake_requester.user,
  415. room_id=room_id,
  416. action="invite",
  417. remote_room_hosts=remote_room_hosts,
  418. ratelimit=False,
  419. )
  420. await self.room_member_handler.update_membership(
  421. requester=fake_requester,
  422. target=fake_requester.user,
  423. room_id=room_id,
  424. action="join",
  425. remote_room_hosts=remote_room_hosts,
  426. ratelimit=False,
  427. )
  428. return HTTPStatus.OK, {"room_id": room_id}
  429. class MakeRoomAdminRestServlet(ResolveRoomIdMixin, RestServlet):
  430. """Allows a server admin to get power in a room if a local user has power in
  431. a room. Will also invite the user if they're not in the room and it's a
  432. private room. Can specify another user (rather than the admin user) to be
  433. granted power, e.g.:
  434. POST/_synapse/admin/v1/rooms/<room_id_or_alias>/make_room_admin
  435. {
  436. "user_id": "@foo:example.com"
  437. }
  438. """
  439. PATTERNS = admin_patterns("/rooms/(?P<room_identifier>[^/]*)/make_room_admin$")
  440. def __init__(self, hs: "HomeServer"):
  441. super().__init__(hs)
  442. self.auth = hs.get_auth()
  443. self.store = hs.get_datastores().main
  444. self._state_storage_controller = hs.get_storage_controllers().state
  445. self.event_creation_handler = hs.get_event_creation_handler()
  446. self.state_handler = hs.get_state_handler()
  447. self.is_mine_id = hs.is_mine_id
  448. async def on_POST(
  449. self, request: SynapseRequest, room_identifier: str
  450. ) -> Tuple[int, JsonDict]:
  451. requester = await self.auth.get_user_by_req(request)
  452. await assert_user_is_admin(self.auth, requester)
  453. content = parse_json_object_from_request(request, allow_empty_body=True)
  454. room_id, _ = await self.resolve_room_id(room_identifier)
  455. # Which user to grant room admin rights to.
  456. user_to_add = content.get("user_id", requester.user.to_string())
  457. # Figure out which local users currently have power in the room, if any.
  458. filtered_room_state = await self._state_storage_controller.get_current_state(
  459. room_id,
  460. StateFilter.from_types(
  461. [
  462. (EventTypes.Create, ""),
  463. (EventTypes.PowerLevels, ""),
  464. (EventTypes.JoinRules, ""),
  465. (EventTypes.Member, user_to_add),
  466. ]
  467. ),
  468. )
  469. if not filtered_room_state:
  470. raise SynapseError(HTTPStatus.BAD_REQUEST, "Server not in room")
  471. create_event = filtered_room_state[(EventTypes.Create, "")]
  472. power_levels = filtered_room_state.get((EventTypes.PowerLevels, ""))
  473. if power_levels is not None:
  474. # We pick the local user with the highest power.
  475. user_power = power_levels.content.get("users", {})
  476. admin_users = [
  477. user_id for user_id in user_power if self.is_mine_id(user_id)
  478. ]
  479. admin_users.sort(key=lambda user: user_power[user])
  480. if not admin_users:
  481. raise SynapseError(
  482. HTTPStatus.BAD_REQUEST, "No local admin user in room"
  483. )
  484. admin_user_id = None
  485. for admin_user in reversed(admin_users):
  486. (
  487. current_membership_type,
  488. _,
  489. ) = await self.store.get_local_current_membership_for_user_in_room(
  490. admin_user, room_id
  491. )
  492. if current_membership_type == "join":
  493. admin_user_id = admin_user
  494. break
  495. if not admin_user_id:
  496. raise SynapseError(
  497. HTTPStatus.BAD_REQUEST,
  498. "No local admin user in room",
  499. )
  500. pl_content = power_levels.content
  501. else:
  502. # If there is no power level events then the creator has rights.
  503. pl_content = {}
  504. admin_user_id = create_event.sender
  505. if not self.is_mine_id(admin_user_id):
  506. raise SynapseError(
  507. HTTPStatus.BAD_REQUEST,
  508. "No local admin user in room",
  509. )
  510. # Grant the user power equal to the room admin by attempting to send an
  511. # updated power level event.
  512. new_pl_content = dict(pl_content)
  513. new_pl_content["users"] = dict(pl_content.get("users", {}))
  514. new_pl_content["users"][user_to_add] = new_pl_content["users"][admin_user_id]
  515. fake_requester = create_requester(
  516. admin_user_id,
  517. authenticated_entity=requester.authenticated_entity,
  518. )
  519. try:
  520. await self.event_creation_handler.create_and_send_nonmember_event(
  521. fake_requester,
  522. event_dict={
  523. "content": new_pl_content,
  524. "sender": admin_user_id,
  525. "type": EventTypes.PowerLevels,
  526. "state_key": "",
  527. "room_id": room_id,
  528. },
  529. )
  530. except AuthError:
  531. # The admin user we found turned out not to have enough power.
  532. raise SynapseError(
  533. HTTPStatus.BAD_REQUEST,
  534. "No local admin user in room with power to update power levels.",
  535. )
  536. # Now we check if the user we're granting admin rights to is already in
  537. # the room. If not and it's not a public room we invite them.
  538. member_event = filtered_room_state.get((EventTypes.Member, user_to_add))
  539. is_joined = False
  540. if member_event:
  541. is_joined = member_event.content["membership"] in (
  542. Membership.JOIN,
  543. Membership.INVITE,
  544. )
  545. if is_joined:
  546. return HTTPStatus.OK, {}
  547. join_rules = filtered_room_state.get((EventTypes.JoinRules, ""))
  548. is_public = False
  549. if join_rules:
  550. is_public = join_rules.content.get("join_rule") == JoinRules.PUBLIC
  551. if is_public:
  552. return HTTPStatus.OK, {}
  553. await self.room_member_handler.update_membership(
  554. fake_requester,
  555. target=UserID.from_string(user_to_add),
  556. room_id=room_id,
  557. action=Membership.INVITE,
  558. )
  559. return HTTPStatus.OK, {}
  560. class ForwardExtremitiesRestServlet(ResolveRoomIdMixin, RestServlet):
  561. """Allows a server admin to get or clear forward extremities.
  562. Clearing does not require restarting the server.
  563. Clear forward extremities:
  564. DELETE /_synapse/admin/v1/rooms/<room_id_or_alias>/forward_extremities
  565. Get forward_extremities:
  566. GET /_synapse/admin/v1/rooms/<room_id_or_alias>/forward_extremities
  567. """
  568. PATTERNS = admin_patterns("/rooms/(?P<room_identifier>[^/]*)/forward_extremities$")
  569. def __init__(self, hs: "HomeServer"):
  570. super().__init__(hs)
  571. self.auth = hs.get_auth()
  572. self.store = hs.get_datastores().main
  573. async def on_DELETE(
  574. self, request: SynapseRequest, room_identifier: str
  575. ) -> Tuple[int, JsonDict]:
  576. await assert_requester_is_admin(self.auth, request)
  577. room_id, _ = await self.resolve_room_id(room_identifier)
  578. deleted_count = await self.store.delete_forward_extremities_for_room(room_id)
  579. return HTTPStatus.OK, {"deleted": deleted_count}
  580. async def on_GET(
  581. self, request: SynapseRequest, room_identifier: str
  582. ) -> Tuple[int, JsonDict]:
  583. await assert_requester_is_admin(self.auth, request)
  584. room_id, _ = await self.resolve_room_id(room_identifier)
  585. extremities = await self.store.get_forward_extremities_for_room(room_id)
  586. result = [
  587. {
  588. "event_id": ex[0],
  589. "state_group": ex[1],
  590. "depth": ex[2],
  591. "received_ts": ex[3],
  592. }
  593. for ex in extremities
  594. ]
  595. return HTTPStatus.OK, {"count": len(extremities), "results": result}
  596. class RoomEventContextServlet(RestServlet):
  597. """
  598. Provide the context for an event.
  599. This API is designed to be used when system administrators wish to look at
  600. an abuse report and understand what happened during and immediately prior
  601. to this event.
  602. """
  603. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)/context/(?P<event_id>[^/]*)$")
  604. def __init__(self, hs: "HomeServer"):
  605. super().__init__()
  606. self._hs = hs
  607. self.clock = hs.get_clock()
  608. self.room_context_handler = hs.get_room_context_handler()
  609. self._event_serializer = hs.get_event_client_serializer()
  610. self.auth = hs.get_auth()
  611. async def on_GET(
  612. self, request: SynapseRequest, room_id: str, event_id: str
  613. ) -> Tuple[int, JsonDict]:
  614. requester = await self.auth.get_user_by_req(request, allow_guest=False)
  615. await assert_user_is_admin(self.auth, requester)
  616. limit = parse_integer(request, "limit", default=10)
  617. # picking the API shape for symmetry with /messages
  618. filter_str = parse_string(request, "filter", encoding="utf-8")
  619. if filter_str:
  620. filter_json = urlparse.unquote(filter_str)
  621. event_filter: Optional[Filter] = Filter(
  622. self._hs, json_decoder.decode(filter_json)
  623. )
  624. else:
  625. event_filter = None
  626. event_context = await self.room_context_handler.get_event_context(
  627. requester,
  628. room_id,
  629. event_id,
  630. limit,
  631. event_filter,
  632. use_admin_priviledge=True,
  633. )
  634. if not event_context:
  635. raise SynapseError(
  636. HTTPStatus.NOT_FOUND, "Event not found.", errcode=Codes.NOT_FOUND
  637. )
  638. time_now = self.clock.time_msec()
  639. results = {
  640. "events_before": await self._event_serializer.serialize_events(
  641. event_context.events_before,
  642. time_now,
  643. bundle_aggregations=event_context.aggregations,
  644. ),
  645. "event": await self._event_serializer.serialize_event(
  646. event_context.event,
  647. time_now,
  648. bundle_aggregations=event_context.aggregations,
  649. ),
  650. "events_after": await self._event_serializer.serialize_events(
  651. event_context.events_after,
  652. time_now,
  653. bundle_aggregations=event_context.aggregations,
  654. ),
  655. "state": await self._event_serializer.serialize_events(
  656. event_context.state, time_now
  657. ),
  658. "start": event_context.start,
  659. "end": event_context.end,
  660. }
  661. return HTTPStatus.OK, results
  662. class BlockRoomRestServlet(RestServlet):
  663. """
  664. Manage blocking of rooms.
  665. On PUT: Add or remove a room from blocking list.
  666. On GET: Get blocking status of room and user who has blocked this room.
  667. """
  668. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)/block$")
  669. def __init__(self, hs: "HomeServer"):
  670. self._auth = hs.get_auth()
  671. self._store = hs.get_datastores().main
  672. async def on_GET(
  673. self, request: SynapseRequest, room_id: str
  674. ) -> Tuple[int, JsonDict]:
  675. await assert_requester_is_admin(self._auth, request)
  676. if not RoomID.is_valid(room_id):
  677. raise SynapseError(
  678. HTTPStatus.BAD_REQUEST, "%s is not a legal room ID" % (room_id,)
  679. )
  680. blocked_by = await self._store.room_is_blocked_by(room_id)
  681. # Test `not None` if `user_id` is an empty string
  682. # if someone add manually an entry in database
  683. if blocked_by is not None:
  684. response = {"block": True, "user_id": blocked_by}
  685. else:
  686. response = {"block": False}
  687. return HTTPStatus.OK, response
  688. async def on_PUT(
  689. self, request: SynapseRequest, room_id: str
  690. ) -> Tuple[int, JsonDict]:
  691. requester = await self._auth.get_user_by_req(request)
  692. await assert_user_is_admin(self._auth, requester)
  693. content = parse_json_object_from_request(request)
  694. if not RoomID.is_valid(room_id):
  695. raise SynapseError(
  696. HTTPStatus.BAD_REQUEST, "%s is not a legal room ID" % (room_id,)
  697. )
  698. assert_params_in_dict(content, ["block"])
  699. block = content.get("block")
  700. if not isinstance(block, bool):
  701. raise SynapseError(
  702. HTTPStatus.BAD_REQUEST,
  703. "Param 'block' must be a boolean.",
  704. Codes.BAD_JSON,
  705. )
  706. if block:
  707. await self._store.block_room(room_id, requester.user.to_string())
  708. else:
  709. await self._store.unblock_room(room_id)
  710. return HTTPStatus.OK, {"block": block}
  711. class RoomMessagesRestServlet(RestServlet):
  712. """
  713. Get messages list of a room.
  714. """
  715. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)/messages$")
  716. def __init__(self, hs: "HomeServer"):
  717. self._hs = hs
  718. self._clock = hs.get_clock()
  719. self._pagination_handler = hs.get_pagination_handler()
  720. self._auth = hs.get_auth()
  721. self._store = hs.get_datastores().main
  722. async def on_GET(
  723. self, request: SynapseRequest, room_id: str
  724. ) -> Tuple[int, JsonDict]:
  725. requester = await self._auth.get_user_by_req(request)
  726. await assert_user_is_admin(self._auth, requester)
  727. pagination_config = await PaginationConfig.from_request(
  728. self._store, request, default_limit=10
  729. )
  730. # Twisted will have processed the args by now.
  731. assert request.args is not None
  732. as_client_event = b"raw" not in request.args
  733. filter_str = parse_string(request, "filter", encoding="utf-8")
  734. if filter_str:
  735. filter_json = urlparse.unquote(filter_str)
  736. event_filter: Optional[Filter] = Filter(
  737. self._hs, json_decoder.decode(filter_json)
  738. )
  739. if (
  740. event_filter
  741. and event_filter.filter_json.get("event_format", "client")
  742. == "federation"
  743. ):
  744. as_client_event = False
  745. else:
  746. event_filter = None
  747. msgs = await self._pagination_handler.get_messages(
  748. room_id=room_id,
  749. requester=requester,
  750. pagin_config=pagination_config,
  751. as_client_event=as_client_event,
  752. event_filter=event_filter,
  753. use_admin_priviledge=True,
  754. )
  755. return HTTPStatus.OK, msgs
  756. class RoomTimestampToEventRestServlet(RestServlet):
  757. """
  758. API endpoint to fetch the `event_id` of the closest event to the given
  759. timestamp (`ts` query parameter) in the given direction (`dir` query
  760. parameter).
  761. Useful for cases like jump to date so you can start paginating messages from
  762. a given date in the archive.
  763. `ts` is a timestamp in milliseconds where we will find the closest event in
  764. the given direction.
  765. `dir` can be `f` or `b` to indicate forwards and backwards in time from the
  766. given timestamp.
  767. GET /_synapse/admin/v1/rooms/<roomID>/timestamp_to_event?ts=<timestamp>&dir=<direction>
  768. {
  769. "event_id": ...
  770. }
  771. """
  772. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)/timestamp_to_event$")
  773. def __init__(self, hs: "HomeServer"):
  774. self._auth = hs.get_auth()
  775. self._store = hs.get_datastores().main
  776. self._timestamp_lookup_handler = hs.get_timestamp_lookup_handler()
  777. async def on_GET(
  778. self, request: SynapseRequest, room_id: str
  779. ) -> Tuple[int, JsonDict]:
  780. requester = await self._auth.get_user_by_req(request)
  781. await assert_user_is_admin(self._auth, requester)
  782. timestamp = parse_integer(request, "ts", required=True)
  783. direction = parse_enum(request, "dir", Direction, default=Direction.FORWARDS)
  784. (
  785. event_id,
  786. origin_server_ts,
  787. ) = await self._timestamp_lookup_handler.get_event_for_timestamp(
  788. requester, room_id, timestamp, direction
  789. )
  790. return HTTPStatus.OK, {
  791. "event_id": event_id,
  792. "origin_server_ts": origin_server_ts,
  793. }