rooms.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2019 The Matrix.org Foundation C.I.C.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. from typing import List, Optional
  17. from synapse.api.constants import EventTypes, JoinRules, Membership
  18. from synapse.api.errors import Codes, NotFoundError, SynapseError
  19. from synapse.http.servlet import (
  20. RestServlet,
  21. assert_params_in_dict,
  22. parse_integer,
  23. parse_json_object_from_request,
  24. parse_string,
  25. )
  26. from synapse.rest.admin._base import (
  27. admin_patterns,
  28. assert_requester_is_admin,
  29. assert_user_is_admin,
  30. historical_admin_path_patterns,
  31. )
  32. from synapse.storage.data_stores.main.room import RoomSortOrder
  33. from synapse.types import RoomAlias, RoomID, UserID, create_requester
  34. from synapse.util.async_helpers import maybe_awaitable
  35. logger = logging.getLogger(__name__)
  36. class ShutdownRoomRestServlet(RestServlet):
  37. """Shuts down a room by removing all local users from the room and blocking
  38. all future invites and joins to the room. Any local aliases will be repointed
  39. to a new room created by `new_room_user_id` and kicked users will be auto
  40. joined to the new room.
  41. """
  42. PATTERNS = historical_admin_path_patterns("/shutdown_room/(?P<room_id>[^/]+)")
  43. DEFAULT_MESSAGE = (
  44. "Sharing illegal content on this server is not permitted and rooms in"
  45. " violation will be blocked."
  46. )
  47. def __init__(self, hs):
  48. self.hs = hs
  49. self.store = hs.get_datastore()
  50. self.state = hs.get_state_handler()
  51. self._room_creation_handler = hs.get_room_creation_handler()
  52. self.event_creation_handler = hs.get_event_creation_handler()
  53. self.room_member_handler = hs.get_room_member_handler()
  54. self.auth = hs.get_auth()
  55. self._replication = hs.get_replication_data_handler()
  56. async def on_POST(self, request, room_id):
  57. requester = await self.auth.get_user_by_req(request)
  58. await assert_user_is_admin(self.auth, requester.user)
  59. content = parse_json_object_from_request(request)
  60. assert_params_in_dict(content, ["new_room_user_id"])
  61. new_room_user_id = content["new_room_user_id"]
  62. room_creator_requester = create_requester(new_room_user_id)
  63. message = content.get("message", self.DEFAULT_MESSAGE)
  64. room_name = content.get("room_name", "Content Violation Notification")
  65. info, stream_id = await self._room_creation_handler.create_room(
  66. room_creator_requester,
  67. config={
  68. "preset": "public_chat",
  69. "name": room_name,
  70. "power_level_content_override": {"users_default": -10},
  71. },
  72. ratelimit=False,
  73. )
  74. new_room_id = info["room_id"]
  75. requester_user_id = requester.user.to_string()
  76. logger.info(
  77. "Shutting down room %r, joining to new room: %r", room_id, new_room_id
  78. )
  79. # This will work even if the room is already blocked, but that is
  80. # desirable in case the first attempt at blocking the room failed below.
  81. await self.store.block_room(room_id, requester_user_id)
  82. # We now wait for the create room to come back in via replication so
  83. # that we can assume that all the joins/invites have propogated before
  84. # we try and auto join below.
  85. #
  86. # TODO: Currently the events stream is written to from master
  87. await self._replication.wait_for_stream_position(
  88. self.hs.config.worker.writers.events, "events", stream_id
  89. )
  90. users = await self.state.get_current_users_in_room(room_id)
  91. kicked_users = []
  92. failed_to_kick_users = []
  93. for user_id in users:
  94. if not self.hs.is_mine_id(user_id):
  95. continue
  96. logger.info("Kicking %r from %r...", user_id, room_id)
  97. try:
  98. target_requester = create_requester(user_id)
  99. _, stream_id = await self.room_member_handler.update_membership(
  100. requester=target_requester,
  101. target=target_requester.user,
  102. room_id=room_id,
  103. action=Membership.LEAVE,
  104. content={},
  105. ratelimit=False,
  106. require_consent=False,
  107. )
  108. # Wait for leave to come in over replication before trying to forget.
  109. await self._replication.wait_for_stream_position(
  110. self.hs.config.worker.writers.events, "events", stream_id
  111. )
  112. await self.room_member_handler.forget(target_requester.user, room_id)
  113. await self.room_member_handler.update_membership(
  114. requester=target_requester,
  115. target=target_requester.user,
  116. room_id=new_room_id,
  117. action=Membership.JOIN,
  118. content={},
  119. ratelimit=False,
  120. require_consent=False,
  121. )
  122. kicked_users.append(user_id)
  123. except Exception:
  124. logger.exception(
  125. "Failed to leave old room and join new room for %r", user_id
  126. )
  127. failed_to_kick_users.append(user_id)
  128. await self.event_creation_handler.create_and_send_nonmember_event(
  129. room_creator_requester,
  130. {
  131. "type": "m.room.message",
  132. "content": {"body": message, "msgtype": "m.text"},
  133. "room_id": new_room_id,
  134. "sender": new_room_user_id,
  135. },
  136. ratelimit=False,
  137. )
  138. aliases_for_room = await maybe_awaitable(
  139. self.store.get_aliases_for_room(room_id)
  140. )
  141. await self.store.update_aliases_for_room(
  142. room_id, new_room_id, requester_user_id
  143. )
  144. return (
  145. 200,
  146. {
  147. "kicked_users": kicked_users,
  148. "failed_to_kick_users": failed_to_kick_users,
  149. "local_aliases": aliases_for_room,
  150. "new_room_id": new_room_id,
  151. },
  152. )
  153. class ListRoomRestServlet(RestServlet):
  154. """
  155. List all rooms that are known to the homeserver. Results are returned
  156. in a dictionary containing room information. Supports pagination.
  157. """
  158. PATTERNS = admin_patterns("/rooms$")
  159. def __init__(self, hs):
  160. self.store = hs.get_datastore()
  161. self.auth = hs.get_auth()
  162. self.admin_handler = hs.get_handlers().admin_handler
  163. async def on_GET(self, request):
  164. requester = await self.auth.get_user_by_req(request)
  165. await assert_user_is_admin(self.auth, requester.user)
  166. # Extract query parameters
  167. start = parse_integer(request, "from", default=0)
  168. limit = parse_integer(request, "limit", default=100)
  169. order_by = parse_string(request, "order_by", default=RoomSortOrder.NAME.value)
  170. if order_by not in (
  171. RoomSortOrder.ALPHABETICAL.value,
  172. RoomSortOrder.SIZE.value,
  173. RoomSortOrder.NAME.value,
  174. RoomSortOrder.CANONICAL_ALIAS.value,
  175. RoomSortOrder.JOINED_MEMBERS.value,
  176. RoomSortOrder.JOINED_LOCAL_MEMBERS.value,
  177. RoomSortOrder.VERSION.value,
  178. RoomSortOrder.CREATOR.value,
  179. RoomSortOrder.ENCRYPTION.value,
  180. RoomSortOrder.FEDERATABLE.value,
  181. RoomSortOrder.PUBLIC.value,
  182. RoomSortOrder.JOIN_RULES.value,
  183. RoomSortOrder.GUEST_ACCESS.value,
  184. RoomSortOrder.HISTORY_VISIBILITY.value,
  185. RoomSortOrder.STATE_EVENTS.value,
  186. ):
  187. raise SynapseError(
  188. 400,
  189. "Unknown value for order_by: %s" % (order_by,),
  190. errcode=Codes.INVALID_PARAM,
  191. )
  192. search_term = parse_string(request, "search_term")
  193. if search_term == "":
  194. raise SynapseError(
  195. 400,
  196. "search_term cannot be an empty string",
  197. errcode=Codes.INVALID_PARAM,
  198. )
  199. direction = parse_string(request, "dir", default="f")
  200. if direction not in ("f", "b"):
  201. raise SynapseError(
  202. 400, "Unknown direction: %s" % (direction,), errcode=Codes.INVALID_PARAM
  203. )
  204. reverse_order = True if direction == "b" else False
  205. # Return list of rooms according to parameters
  206. rooms, total_rooms = await self.store.get_rooms_paginate(
  207. start, limit, order_by, reverse_order, search_term
  208. )
  209. response = {
  210. # next_token should be opaque, so return a value the client can parse
  211. "offset": start,
  212. "rooms": rooms,
  213. "total_rooms": total_rooms,
  214. }
  215. # Are there more rooms to paginate through after this?
  216. if (start + limit) < total_rooms:
  217. # There are. Calculate where the query should start from next time
  218. # to get the next part of the list
  219. response["next_batch"] = start + limit
  220. # Is it possible to paginate backwards? Check if we currently have an
  221. # offset
  222. if start > 0:
  223. if start > limit:
  224. # Going back one iteration won't take us to the start.
  225. # Calculate new offset
  226. response["prev_batch"] = start - limit
  227. else:
  228. response["prev_batch"] = 0
  229. return 200, response
  230. class RoomRestServlet(RestServlet):
  231. """Get room details.
  232. TODO: Add on_POST to allow room creation without joining the room
  233. """
  234. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)$")
  235. def __init__(self, hs):
  236. self.hs = hs
  237. self.auth = hs.get_auth()
  238. self.store = hs.get_datastore()
  239. async def on_GET(self, request, room_id):
  240. await assert_requester_is_admin(self.auth, request)
  241. ret = await self.store.get_room_with_stats(room_id)
  242. if not ret:
  243. raise NotFoundError("Room not found")
  244. return 200, ret
  245. class JoinRoomAliasServlet(RestServlet):
  246. PATTERNS = admin_patterns("/join/(?P<room_identifier>[^/]*)")
  247. def __init__(self, hs):
  248. self.hs = hs
  249. self.auth = hs.get_auth()
  250. self.room_member_handler = hs.get_room_member_handler()
  251. self.admin_handler = hs.get_handlers().admin_handler
  252. self.state_handler = hs.get_state_handler()
  253. async def on_POST(self, request, room_identifier):
  254. requester = await self.auth.get_user_by_req(request)
  255. await assert_user_is_admin(self.auth, requester.user)
  256. content = parse_json_object_from_request(request)
  257. assert_params_in_dict(content, ["user_id"])
  258. target_user = UserID.from_string(content["user_id"])
  259. if not self.hs.is_mine(target_user):
  260. raise SynapseError(400, "This endpoint can only be used with local users")
  261. if not await self.admin_handler.get_user(target_user):
  262. raise NotFoundError("User not found")
  263. if RoomID.is_valid(room_identifier):
  264. room_id = room_identifier
  265. try:
  266. remote_room_hosts = [
  267. x.decode("ascii") for x in request.args[b"server_name"]
  268. ] # type: Optional[List[str]]
  269. except Exception:
  270. remote_room_hosts = None
  271. elif RoomAlias.is_valid(room_identifier):
  272. handler = self.room_member_handler
  273. room_alias = RoomAlias.from_string(room_identifier)
  274. room_id, remote_room_hosts = await handler.lookup_room_alias(room_alias)
  275. room_id = room_id.to_string()
  276. else:
  277. raise SynapseError(
  278. 400, "%s was not legal room ID or room alias" % (room_identifier,)
  279. )
  280. fake_requester = create_requester(target_user)
  281. # send invite if room has "JoinRules.INVITE"
  282. room_state = await self.state_handler.get_current_state(room_id)
  283. join_rules_event = room_state.get((EventTypes.JoinRules, ""))
  284. if join_rules_event:
  285. if not (join_rules_event.content.get("join_rule") == JoinRules.PUBLIC):
  286. await self.room_member_handler.update_membership(
  287. requester=requester,
  288. target=fake_requester.user,
  289. room_id=room_id,
  290. action="invite",
  291. remote_room_hosts=remote_room_hosts,
  292. ratelimit=False,
  293. )
  294. await self.room_member_handler.update_membership(
  295. requester=fake_requester,
  296. target=fake_requester.user,
  297. room_id=room_id,
  298. action="join",
  299. remote_room_hosts=remote_room_hosts,
  300. ratelimit=False,
  301. )
  302. return 200, {"room_id": room_id}