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

Add delete room admin endpoint #7613

Merged
merged 15 commits into from
Jul 14, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/7613.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add delete room admin endpoint (`POST /_synapse/admin/v1/rooms/<room_id>/delete`). Contributed by @dklimpel.
2 changes: 2 additions & 0 deletions docs/admin_api/purge_room.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ This API will remove all trace of a room from your database.

All local users must have left the room before it can be removed.

See also: [Delete Room API](rooms.md#delete-room-api)

The API is:

```
Expand Down
93 changes: 93 additions & 0 deletions docs/admin_api/rooms.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,96 @@ Response:
"state_events": 93534
}
```

# Delete Room API

The Delete Room admin API allows server admins to remove rooms from server
and block these rooms.
It is a combination and improvement of "[Shutdown room](shutdown_room.md)"
and "[Purge room](purge_room.md)" API.

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.

If `block` is `True` it prevents new joins to the old room.

This API will remove all trace of the old room from your database after removing
all local users.
Depending on the amount of history being purged a call to the API may take
several minutes or longer.

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.

The API is:

```json
POST /_synapse/admin/v1/rooms/<room_id>/delete
```

richvdh marked this conversation as resolved.
Show resolved Hide resolved
with a body of:
```json
{
"new_room_user_id": "@someuser:example.com",
"room_name": "Content Violation Notification",
"message": "Bad Room has been shutdown due to content violations on this server. Please review our Terms of Service.",
"block": true
}
```

To use it, you will need to authenticate by providing an ``access_token`` for a
server admin: see [README.rst](README.rst).

A response body like the following is returned:
richvdh marked this conversation as resolved.
Show resolved Hide resolved

```json
{
"kicked_users": [
"@foobar:example.com"
],
"failed_to_kick_users": [],
"local_aliases": [
"#badroom:example.com",
"#evilsaloon:example.com"
],
"new_room_id": "!newroomid:example.com"
}
```

## Parameters

The following parameters should be set in the URL:

* `room_id` - The ID of the room.

The following JSON body parameters are available:

* `new_room_user_id` - Optional. 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. The user ID must be from local server but it does not have to exist.
richvdh marked this conversation as resolved.
Show resolved Hide resolved
* `room_name` - Optional. A string representing the name of the room that new users will be
invited to. Defaults to `Content Violation Notification`
* `message` - Optional. 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` - Optional. If set to `true`, this room will be added to a blocking list, preventing future attempts to
join the room. Defaults to `false`.

The JSON body must not be empty. The body must be at least `{}`.

## Response

The following fields are returned in the JSON response body:

* `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.
2 changes: 2 additions & 0 deletions docs/admin_api/shutdown_room.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ disallow any further invites or joins.
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.

See also: [Delete Room API](rooms.md#delete-room-api)

## API

You will need to authenticate with an access token for an admin user.
Expand Down
208 changes: 206 additions & 2 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
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,
)
Expand All @@ -43,9 +44,10 @@
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

Expand Down Expand Up @@ -1089,3 +1091,205 @@ def get_current_key(self):

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,
}
2 changes: 2 additions & 0 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
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,
Expand Down Expand Up @@ -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)
Expand Down
Loading