diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 950a84acd0..fb37d371ad 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -22,11 +22,12 @@ import logging
import math
import string
from collections import OrderedDict
-from typing import Tuple
+from typing import Optional, Tuple
from synapse.api.constants import (
EventTypes,
JoinRules,
+ Membership,
RoomCreationPreset,
RoomEncryptionAlgorithms,
)
@@ -43,9 +44,10 @@ from synapse.types import (
StateMap,
StreamToken,
UserID,
+ create_requester,
)
from synapse.util import stringutils
-from synapse.util.async_helpers import Linearizer
+from synapse.util.async_helpers import Linearizer, maybe_awaitable
from synapse.util.caches.response_cache import ResponseCache
from synapse.visibility import filter_events_for_client
@@ -1089,3 +1091,205 @@ class RoomEventSource(object):
def get_current_key_for_room(self, room_id):
return self.store.get_room_events_max_id(room_id)
+
+
+class RoomShutdownHandler(object):
+
+ DEFAULT_MESSAGE = (
+ "Sharing illegal content on this server is not permitted and rooms in"
+ " violation will be blocked."
+ )
+ DEFAULT_ROOM_NAME = "Content Violation Notification"
+
+ def __init__(self, hs):
+ self.hs = hs
+ self.room_member_handler = hs.get_room_member_handler()
+ self._room_creation_handler = hs.get_room_creation_handler()
+ self._replication = hs.get_replication_data_handler()
+ self.event_creation_handler = hs.get_event_creation_handler()
+ self.state = hs.get_state_handler()
+ self.store = hs.get_datastore()
+
+ async def shutdown_room(
+ self,
+ room_id: str,
+ requester_user_id: str,
+ new_room_user_id: Optional[str] = None,
+ new_room_name: Optional[str] = None,
+ message: Optional[str] = None,
+ block: bool = False,
+ ) -> dict:
+ """
+ Shuts down a room. Moves all local users and room aliases automatically
+ to a new room if `new_room_user_id` is set. Otherwise local users only
+ leave the room without any information.
+
+ The new room will be created with the user specified by the
+ `new_room_user_id` parameter as room administrator and will contain a
+ message explaining what happened. Users invited to the new room will
+ have power level `-10` by default, and thus be unable to speak.
+
+ The local server will only have the power to move local user and room
+ aliases to the new room. Users on other servers will be unaffected.
+
+ Args:
+ room_id: The ID of the room to shut down.
+ requester_user_id:
+ User who requested the action and put the room on the
+ blocking list.
+ new_room_user_id:
+ If set, a new room will be created with this user ID
+ as the creator and admin, and all users in the old room will be
+ moved into that room. If not set, no new room will be created
+ and the users will just be removed from the old room.
+ new_room_name:
+ A string representing the name of the room that new users will
+ be invited to. Defaults to `Content Violation Notification`
+ message:
+ A string containing the first message that will be sent as
+ `new_room_user_id` in the new room. Ideally this will clearly
+ convey why the original room was shut down.
+ Defaults to `Sharing illegal content on this server is not
+ permitted and rooms in violation will be blocked.`
+ block:
+ If set to `true`, this room will be added to a blocking list,
+ preventing future attempts to join the room. Defaults to `false`.
+
+ Returns: a dict containing the following keys:
+ kicked_users: An array of users (`user_id`) that were kicked.
+ failed_to_kick_users:
+ An array of users (`user_id`) that that were not kicked.
+ local_aliases:
+ An array of strings representing the local aliases that were
+ migrated from the old room to the new.
+ new_room_id: A string representing the room ID of the new room.
+ """
+
+ if not new_room_name:
+ new_room_name = self.DEFAULT_ROOM_NAME
+ if not message:
+ message = self.DEFAULT_MESSAGE
+
+ if not RoomID.is_valid(room_id):
+ raise SynapseError(400, "%s is not a legal room ID" % (room_id,))
+
+ if not await self.store.get_room(room_id):
+ raise NotFoundError("Unknown room id %s" % (room_id,))
+
+ # This will work even if the room is already blocked, but that is
+ # desirable in case the first attempt at blocking the room failed below.
+ if block:
+ await self.store.block_room(room_id, requester_user_id)
+
+ if new_room_user_id is not None:
+ if not self.hs.is_mine_id(new_room_user_id):
+ raise SynapseError(
+ 400, "User must be our own: %s" % (new_room_user_id,)
+ )
+
+ room_creator_requester = create_requester(new_room_user_id)
+
+ info, stream_id = await self._room_creation_handler.create_room(
+ room_creator_requester,
+ config={
+ "preset": RoomCreationPreset.PUBLIC_CHAT,
+ "name": new_room_name,
+ "power_level_content_override": {"users_default": -10},
+ },
+ ratelimit=False,
+ )
+ new_room_id = info["room_id"]
+
+ logger.info(
+ "Shutting down room %r, joining to new room: %r", room_id, new_room_id
+ )
+
+ # We now wait for the create room to come back in via replication so
+ # that we can assume that all the joins/invites have propogated before
+ # we try and auto join below.
+ #
+ # TODO: Currently the events stream is written to from master
+ await self._replication.wait_for_stream_position(
+ self.hs.config.worker.writers.events, "events", stream_id
+ )
+ else:
+ new_room_id = None
+ logger.info("Shutting down room %r", room_id)
+
+ users = await self.state.get_current_users_in_room(room_id)
+ kicked_users = []
+ failed_to_kick_users = []
+ for user_id in users:
+ if not self.hs.is_mine_id(user_id):
+ continue
+
+ logger.info("Kicking %r from %r...", user_id, room_id)
+
+ try:
+ # Kick users from room
+ target_requester = create_requester(user_id)
+ _, stream_id = await self.room_member_handler.update_membership(
+ requester=target_requester,
+ target=target_requester.user,
+ room_id=room_id,
+ action=Membership.LEAVE,
+ content={},
+ ratelimit=False,
+ require_consent=False,
+ )
+
+ # Wait for leave to come in over replication before trying to forget.
+ await self._replication.wait_for_stream_position(
+ self.hs.config.worker.writers.events, "events", stream_id
+ )
+
+ await self.room_member_handler.forget(target_requester.user, room_id)
+
+ # Join users to new room
+ if new_room_user_id:
+ await self.room_member_handler.update_membership(
+ requester=target_requester,
+ target=target_requester.user,
+ room_id=new_room_id,
+ action=Membership.JOIN,
+ content={},
+ ratelimit=False,
+ require_consent=False,
+ )
+
+ kicked_users.append(user_id)
+ except Exception:
+ logger.exception(
+ "Failed to leave old room and join new room for %r", user_id
+ )
+ failed_to_kick_users.append(user_id)
+
+ # Send message in new room and move aliases
+ if new_room_user_id:
+ await self.event_creation_handler.create_and_send_nonmember_event(
+ room_creator_requester,
+ {
+ "type": "m.room.message",
+ "content": {"body": message, "msgtype": "m.text"},
+ "room_id": new_room_id,
+ "sender": new_room_user_id,
+ },
+ ratelimit=False,
+ )
+
+ aliases_for_room = await maybe_awaitable(
+ self.store.get_aliases_for_room(room_id)
+ )
+
+ await self.store.update_aliases_for_room(
+ room_id, new_room_id, requester_user_id
+ )
+ else:
+ aliases_for_room = []
+
+ return {
+ "kicked_users": kicked_users,
+ "failed_to_kick_users": failed_to_kick_users,
+ "local_aliases": aliases_for_room,
+ "new_room_id": new_room_id,
+ }
diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py
index 9eda592de9..dc373bc5a3 100644
--- a/synapse/rest/admin/__init__.py
+++ b/synapse/rest/admin/__init__.py
@@ -35,6 +35,7 @@ from synapse.rest.admin.groups import DeleteGroupAdminRestServlet
from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo
from synapse.rest.admin.purge_room_servlet import PurgeRoomServlet
from synapse.rest.admin.rooms import (
+ DeleteRoomRestServlet,
JoinRoomAliasServlet,
ListRoomRestServlet,
RoomRestServlet,
@@ -200,6 +201,7 @@ def register_servlets(hs, http_server):
register_servlets_for_client_rest_resource(hs, http_server)
ListRoomRestServlet(hs).register(http_server)
RoomRestServlet(hs).register(http_server)
+ DeleteRoomRestServlet(hs).register(http_server)
JoinRoomAliasServlet(hs).register(http_server)
PurgeRoomServlet(hs).register(http_server)
SendServerNoticeServlet(hs).register(http_server)
diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py
index e07c32118d..544be47060 100644
--- a/synapse/rest/admin/rooms.py
+++ b/synapse/rest/admin/rooms.py
@@ -13,9 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
+from http import HTTPStatus
from typing import List, Optional
-from synapse.api.constants import EventTypes, JoinRules, Membership, RoomCreationPreset
+from synapse.api.constants import EventTypes, JoinRules
from synapse.api.errors import Codes, NotFoundError, SynapseError
from synapse.http.servlet import (
RestServlet,
@@ -32,7 +33,6 @@ from synapse.rest.admin._base import (
)
from synapse.storage.data_stores.main.room import RoomSortOrder
from synapse.types import RoomAlias, RoomID, UserID, create_requester
-from synapse.util.async_helpers import maybe_awaitable
logger = logging.getLogger(__name__)
@@ -46,20 +46,10 @@ class ShutdownRoomRestServlet(RestServlet):
PATTERNS = historical_admin_path_patterns("/shutdown_room/(?P<room_id>[^/]+)")
- DEFAULT_MESSAGE = (
- "Sharing illegal content on this server is not permitted and rooms in"
- " violation will be blocked."
- )
-
def __init__(self, hs):
self.hs = hs
- self.store = hs.get_datastore()
- self.state = hs.get_state_handler()
- self._room_creation_handler = hs.get_room_creation_handler()
- self.event_creation_handler = hs.get_event_creation_handler()
- self.room_member_handler = hs.get_room_member_handler()
self.auth = hs.get_auth()
- self._replication = hs.get_replication_data_handler()
+ self.room_shutdown_handler = hs.get_room_shutdown_handler()
async def on_POST(self, request, room_id):
requester = await self.auth.get_user_by_req(request)
@@ -67,116 +57,65 @@ class ShutdownRoomRestServlet(RestServlet):
content = parse_json_object_from_request(request)
assert_params_in_dict(content, ["new_room_user_id"])
- new_room_user_id = content["new_room_user_id"]
-
- room_creator_requester = create_requester(new_room_user_id)
-
- message = content.get("message", self.DEFAULT_MESSAGE)
- room_name = content.get("room_name", "Content Violation Notification")
- info, stream_id = await self._room_creation_handler.create_room(
- room_creator_requester,
- config={
- "preset": RoomCreationPreset.PUBLIC_CHAT,
- "name": room_name,
- "power_level_content_override": {"users_default": -10},
- },
- ratelimit=False,
+ ret = await self.room_shutdown_handler.shutdown_room(
+ room_id=room_id,
+ new_room_user_id=content["new_room_user_id"],
+ new_room_name=content.get("room_name"),
+ message=content.get("message"),
+ requester_user_id=requester.user.to_string(),
+ block=True,
)
- new_room_id = info["room_id"]
- requester_user_id = requester.user.to_string()
+ return (200, ret)
- logger.info(
- "Shutting down room %r, joining to new room: %r", room_id, new_room_id
- )
-
- # This will work even if the room is already blocked, but that is
- # desirable in case the first attempt at blocking the room failed below.
- await self.store.block_room(room_id, requester_user_id)
-
- # We now wait for the create room to come back in via replication so
- # that we can assume that all the joins/invites have propogated before
- # we try and auto join below.
- #
- # TODO: Currently the events stream is written to from master
- await self._replication.wait_for_stream_position(
- self.hs.config.worker.writers.events, "events", stream_id
- )
- users = await self.state.get_current_users_in_room(room_id)
- kicked_users = []
- failed_to_kick_users = []
- for user_id in users:
- if not self.hs.is_mine_id(user_id):
- continue
+class DeleteRoomRestServlet(RestServlet):
+ """Delete a room from server. It is a combination and improvement of
+ shut down and purge room.
+ Shuts down a room by removing all local users from the room.
+ Blocking all future invites and joins to the room is optional.
+ If desired any local aliases will be repointed to a new room
+ created by `new_room_user_id` and kicked users will be auto
+ joined to the new room.
+ It will remove all trace of a room from the database.
+ """
- logger.info("Kicking %r from %r...", user_id, room_id)
+ PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)/delete$")
- try:
- target_requester = create_requester(user_id)
- _, stream_id = await self.room_member_handler.update_membership(
- requester=target_requester,
- target=target_requester.user,
- room_id=room_id,
- action=Membership.LEAVE,
- content={},
- ratelimit=False,
- require_consent=False,
- )
-
- # Wait for leave to come in over replication before trying to forget.
- await self._replication.wait_for_stream_position(
- self.hs.config.worker.writers.events, "events", stream_id
- )
+ def __init__(self, hs):
+ self.hs = hs
+ self.auth = hs.get_auth()
+ self.room_shutdown_handler = hs.get_room_shutdown_handler()
+ self.pagination_handler = hs.get_pagination_handler()
- await self.room_member_handler.forget(target_requester.user, room_id)
+ async def on_POST(self, request, room_id):
+ requester = await self.auth.get_user_by_req(request)
+ await assert_user_is_admin(self.auth, requester.user)
- await self.room_member_handler.update_membership(
- requester=target_requester,
- target=target_requester.user,
- room_id=new_room_id,
- action=Membership.JOIN,
- content={},
- ratelimit=False,
- require_consent=False,
- )
+ content = parse_json_object_from_request(request)
- kicked_users.append(user_id)
- except Exception:
- logger.exception(
- "Failed to leave old room and join new room for %r", user_id
- )
- failed_to_kick_users.append(user_id)
-
- await self.event_creation_handler.create_and_send_nonmember_event(
- room_creator_requester,
- {
- "type": "m.room.message",
- "content": {"body": message, "msgtype": "m.text"},
- "room_id": new_room_id,
- "sender": new_room_user_id,
- },
- ratelimit=False,
- )
+ block = content.get("block", False)
+ if not isinstance(block, bool):
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST,
+ "Param 'block' must be a boolean, if given",
+ Codes.BAD_JSON,
+ )
- aliases_for_room = await maybe_awaitable(
- self.store.get_aliases_for_room(room_id)
+ ret = await self.room_shutdown_handler.shutdown_room(
+ room_id=room_id,
+ new_room_user_id=content.get("new_room_user_id"),
+ new_room_name=content.get("room_name"),
+ message=content.get("message"),
+ requester_user_id=requester.user.to_string(),
+ block=block,
)
- await self.store.update_aliases_for_room(
- room_id, new_room_id, requester_user_id
- )
+ # Purge room
+ await self.pagination_handler.purge_room(room_id)
- return (
- 200,
- {
- "kicked_users": kicked_users,
- "failed_to_kick_users": failed_to_kick_users,
- "local_aliases": aliases_for_room,
- "new_room_id": new_room_id,
- },
- )
+ return (200, ret)
class ListRoomRestServlet(RestServlet):
diff --git a/synapse/server.py b/synapse/server.py
index 6acce2e23f..d5ebaea7f7 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -73,7 +73,11 @@ from synapse.handlers.profile import BaseProfileHandler, MasterProfileHandler
from synapse.handlers.read_marker import ReadMarkerHandler
from synapse.handlers.receipts import ReceiptsHandler
from synapse.handlers.register import RegistrationHandler
-from synapse.handlers.room import RoomContextHandler, RoomCreationHandler
+from synapse.handlers.room import (
+ RoomContextHandler,
+ RoomCreationHandler,
+ RoomShutdownHandler,
+)
from synapse.handlers.room_list import RoomListHandler
from synapse.handlers.room_member import RoomMemberMasterHandler
from synapse.handlers.room_member_worker import RoomMemberWorkerHandler
@@ -144,6 +148,7 @@ class HomeServer(object):
"handlers",
"auth",
"room_creation_handler",
+ "room_shutdown_handler",
"state_handler",
"state_resolution_handler",
"presence_handler",
@@ -357,6 +362,9 @@ class HomeServer(object):
def build_room_creation_handler(self):
return RoomCreationHandler(self)
+ def build_room_shutdown_handler(self):
+ return RoomShutdownHandler(self)
+
def build_sendmail(self):
return sendmail
diff --git a/synapse/server.pyi b/synapse/server.pyi
index fe8024d2d4..58cd099e6d 100644
--- a/synapse/server.pyi
+++ b/synapse/server.pyi
@@ -71,6 +71,8 @@ class HomeServer(object):
pass
def get_room_member_handler(self) -> synapse.handlers.room_member.RoomMemberHandler:
pass
+ def get_room_shutdown_handler(self) -> synapse.handlers.room.RoomShutdownHandler:
+ pass
def get_event_creation_handler(
self,
) -> synapse.handlers.message.EventCreationHandler:
diff --git a/synapse/storage/data_stores/main/room.py b/synapse/storage/data_stores/main/room.py
index c473cf158f..dace20e6db 100644
--- a/synapse/storage/data_stores/main/room.py
+++ b/synapse/storage/data_stores/main/room.py
@@ -118,7 +118,12 @@ class RoomWorkerStore(SQLBaseStore):
WHERE room_id = ?
"""
txn.execute(sql, [room_id])
- res = self.db.cursor_to_dict(txn)[0]
+ # Catch error if sql returns empty result to return "None" instead of an error
+ try:
+ res = self.db.cursor_to_dict(txn)[0]
+ except IndexError:
+ return None
+
res["federatable"] = bool(res["federatable"])
res["public"] = bool(res["public"])
return res
|