Add delete room admin endpoint (#7613)
The Delete Room admin API allows server admins to remove rooms from server
and block these rooms.
`DELETE /_synapse/admin/v1/rooms/<room_id>`
It is a combination and improvement of "[Shutdown room](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/shutdown_room.md)" and "[Purge room](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/purge_room.md)" API.
Fixes: #6425
It also fixes a bug in [synapse/storage/data_stores/main/room.py](synapse/storage/data_stores/main/room.py) in ` get_room_with_stats`.
It should return `None` if the room is unknown. But it returns an `IndexError`.
901b1fa561/synapse/storage/data_stores/main/room.py (L99-L105)
Related to:
- #5575
- https://github.com/Awesome-Technologies/synapse-admin/issues/17
Signed-off-by: Dirk Klimpel dirk@klimpel.org
pull/7848/head
parent
77d2c05410
commit
491f0dab1b
|
@ -0,0 +1 @@
|
||||||
|
Add delete room admin endpoint (`POST /_synapse/admin/v1/rooms/<room_id>/delete`). Contributed by @dklimpel.
|
|
@ -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.
|
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:
|
The API is:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -318,3 +318,97 @@ Response:
|
||||||
"state_events": 93534
|
"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
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```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 on the local server, but does not necessarily
|
||||||
|
have to belong to a registered user.
|
||||||
|
* `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.
|
||||||
|
|
|
@ -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 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 new room. Users on other servers will be unaffected.
|
||||||
|
|
||||||
|
See also: [Delete Room API](rooms.md#delete-room-api)
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
You will need to authenticate with an access token for an admin user.
|
You will need to authenticate with an access token for an admin user.
|
||||||
|
|
|
@ -22,11 +22,12 @@ import logging
|
||||||
import math
|
import math
|
||||||
import string
|
import string
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from synapse.api.constants import (
|
from synapse.api.constants import (
|
||||||
EventTypes,
|
EventTypes,
|
||||||
JoinRules,
|
JoinRules,
|
||||||
|
Membership,
|
||||||
RoomCreationPreset,
|
RoomCreationPreset,
|
||||||
RoomEncryptionAlgorithms,
|
RoomEncryptionAlgorithms,
|
||||||
)
|
)
|
||||||
|
@ -43,9 +44,10 @@ from synapse.types import (
|
||||||
StateMap,
|
StateMap,
|
||||||
StreamToken,
|
StreamToken,
|
||||||
UserID,
|
UserID,
|
||||||
|
create_requester,
|
||||||
)
|
)
|
||||||
from synapse.util import stringutils
|
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.util.caches.response_cache import ResponseCache
|
||||||
from synapse.visibility import filter_events_for_client
|
from synapse.visibility import filter_events_for_client
|
||||||
|
|
||||||
|
@ -1089,3 +1091,205 @@ class RoomEventSource(object):
|
||||||
|
|
||||||
def get_current_key_for_room(self, room_id):
|
def get_current_key_for_room(self, room_id):
|
||||||
return self.store.get_room_events_max_id(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,
|
||||||
|
}
|
||||||
|
|
|
@ -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.media import ListMediaInRoom, register_servlets_for_media_repo
|
||||||
from synapse.rest.admin.purge_room_servlet import PurgeRoomServlet
|
from synapse.rest.admin.purge_room_servlet import PurgeRoomServlet
|
||||||
from synapse.rest.admin.rooms import (
|
from synapse.rest.admin.rooms import (
|
||||||
|
DeleteRoomRestServlet,
|
||||||
JoinRoomAliasServlet,
|
JoinRoomAliasServlet,
|
||||||
ListRoomRestServlet,
|
ListRoomRestServlet,
|
||||||
RoomRestServlet,
|
RoomRestServlet,
|
||||||
|
@ -200,6 +201,7 @@ def register_servlets(hs, http_server):
|
||||||
register_servlets_for_client_rest_resource(hs, http_server)
|
register_servlets_for_client_rest_resource(hs, http_server)
|
||||||
ListRoomRestServlet(hs).register(http_server)
|
ListRoomRestServlet(hs).register(http_server)
|
||||||
RoomRestServlet(hs).register(http_server)
|
RoomRestServlet(hs).register(http_server)
|
||||||
|
DeleteRoomRestServlet(hs).register(http_server)
|
||||||
JoinRoomAliasServlet(hs).register(http_server)
|
JoinRoomAliasServlet(hs).register(http_server)
|
||||||
PurgeRoomServlet(hs).register(http_server)
|
PurgeRoomServlet(hs).register(http_server)
|
||||||
SendServerNoticeServlet(hs).register(http_server)
|
SendServerNoticeServlet(hs).register(http_server)
|
||||||
|
|
|
@ -13,9 +13,10 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import logging
|
import logging
|
||||||
|
from http import HTTPStatus
|
||||||
from typing import List, Optional
|
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.api.errors import Codes, NotFoundError, SynapseError
|
||||||
from synapse.http.servlet import (
|
from synapse.http.servlet import (
|
||||||
RestServlet,
|
RestServlet,
|
||||||
|
@ -32,7 +33,6 @@ from synapse.rest.admin._base import (
|
||||||
)
|
)
|
||||||
from synapse.storage.data_stores.main.room import RoomSortOrder
|
from synapse.storage.data_stores.main.room import RoomSortOrder
|
||||||
from synapse.types import RoomAlias, RoomID, UserID, create_requester
|
from synapse.types import RoomAlias, RoomID, UserID, create_requester
|
||||||
from synapse.util.async_helpers import maybe_awaitable
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -46,20 +46,10 @@ class ShutdownRoomRestServlet(RestServlet):
|
||||||
|
|
||||||
PATTERNS = historical_admin_path_patterns("/shutdown_room/(?P<room_id>[^/]+)")
|
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):
|
def __init__(self, hs):
|
||||||
self.hs = 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.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):
|
async def on_POST(self, request, room_id):
|
||||||
requester = await self.auth.get_user_by_req(request)
|
requester = await self.auth.get_user_by_req(request)
|
||||||
|
@ -67,116 +57,65 @@ class ShutdownRoomRestServlet(RestServlet):
|
||||||
|
|
||||||
content = parse_json_object_from_request(request)
|
content = parse_json_object_from_request(request)
|
||||||
assert_params_in_dict(content, ["new_room_user_id"])
|
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)
|
ret = await self.room_shutdown_handler.shutdown_room(
|
||||||
|
room_id=room_id,
|
||||||
message = content.get("message", self.DEFAULT_MESSAGE)
|
new_room_user_id=content["new_room_user_id"],
|
||||||
room_name = content.get("room_name", "Content Violation Notification")
|
new_room_name=content.get("room_name"),
|
||||||
|
message=content.get("message"),
|
||||||
info, stream_id = await self._room_creation_handler.create_room(
|
requester_user_id=requester.user.to_string(),
|
||||||
room_creator_requester,
|
block=True,
|
||||||
config={
|
|
||||||
"preset": RoomCreationPreset.PUBLIC_CHAT,
|
|
||||||
"name": room_name,
|
|
||||||
"power_level_content_override": {"users_default": -10},
|
|
||||||
},
|
|
||||||
ratelimit=False,
|
|
||||||
)
|
|
||||||
new_room_id = info["room_id"]
|
|
||||||
|
|
||||||
requester_user_id = requester.user.to_string()
|
|
||||||
|
|
||||||
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
|
return (200, ret)
|
||||||
# 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
|
class DeleteRoomRestServlet(RestServlet):
|
||||||
# we try and auto join below.
|
"""Delete a room from server. It is a combination and improvement of
|
||||||
#
|
shut down and purge room.
|
||||||
# TODO: Currently the events stream is written to from master
|
Shuts down a room by removing all local users from the room.
|
||||||
await self._replication.wait_for_stream_position(
|
Blocking all future invites and joins to the room is optional.
|
||||||
self.hs.config.worker.writers.events, "events", stream_id
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)/delete$")
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
content = parse_json_object_from_request(request)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
users = await self.state.get_current_users_in_room(room_id)
|
# Purge room
|
||||||
kicked_users = []
|
await self.pagination_handler.purge_room(room_id)
|
||||||
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)
|
return (200, ret)
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.room_member_handler.forget(target_requester.user, room_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)
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ListRoomRestServlet(RestServlet):
|
class ListRoomRestServlet(RestServlet):
|
||||||
|
|
|
@ -73,7 +73,11 @@ from synapse.handlers.profile import BaseProfileHandler, MasterProfileHandler
|
||||||
from synapse.handlers.read_marker import ReadMarkerHandler
|
from synapse.handlers.read_marker import ReadMarkerHandler
|
||||||
from synapse.handlers.receipts import ReceiptsHandler
|
from synapse.handlers.receipts import ReceiptsHandler
|
||||||
from synapse.handlers.register import RegistrationHandler
|
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_list import RoomListHandler
|
||||||
from synapse.handlers.room_member import RoomMemberMasterHandler
|
from synapse.handlers.room_member import RoomMemberMasterHandler
|
||||||
from synapse.handlers.room_member_worker import RoomMemberWorkerHandler
|
from synapse.handlers.room_member_worker import RoomMemberWorkerHandler
|
||||||
|
@ -144,6 +148,7 @@ class HomeServer(object):
|
||||||
"handlers",
|
"handlers",
|
||||||
"auth",
|
"auth",
|
||||||
"room_creation_handler",
|
"room_creation_handler",
|
||||||
|
"room_shutdown_handler",
|
||||||
"state_handler",
|
"state_handler",
|
||||||
"state_resolution_handler",
|
"state_resolution_handler",
|
||||||
"presence_handler",
|
"presence_handler",
|
||||||
|
@ -357,6 +362,9 @@ class HomeServer(object):
|
||||||
def build_room_creation_handler(self):
|
def build_room_creation_handler(self):
|
||||||
return RoomCreationHandler(self)
|
return RoomCreationHandler(self)
|
||||||
|
|
||||||
|
def build_room_shutdown_handler(self):
|
||||||
|
return RoomShutdownHandler(self)
|
||||||
|
|
||||||
def build_sendmail(self):
|
def build_sendmail(self):
|
||||||
return sendmail
|
return sendmail
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,8 @@ class HomeServer(object):
|
||||||
pass
|
pass
|
||||||
def get_room_member_handler(self) -> synapse.handlers.room_member.RoomMemberHandler:
|
def get_room_member_handler(self) -> synapse.handlers.room_member.RoomMemberHandler:
|
||||||
pass
|
pass
|
||||||
|
def get_room_shutdown_handler(self) -> synapse.handlers.room.RoomShutdownHandler:
|
||||||
|
pass
|
||||||
def get_event_creation_handler(
|
def get_event_creation_handler(
|
||||||
self,
|
self,
|
||||||
) -> synapse.handlers.message.EventCreationHandler:
|
) -> synapse.handlers.message.EventCreationHandler:
|
||||||
|
|
|
@ -118,7 +118,12 @@ class RoomWorkerStore(SQLBaseStore):
|
||||||
WHERE room_id = ?
|
WHERE room_id = ?
|
||||||
"""
|
"""
|
||||||
txn.execute(sql, [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["federatable"] = bool(res["federatable"])
|
||||||
res["public"] = bool(res["public"])
|
res["public"] = bool(res["public"])
|
||||||
return res
|
return res
|
||||||
|
|
|
@ -151,6 +151,401 @@ class ShutdownRoomTestCase(unittest.HomeserverTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteRoomTestCase(unittest.HomeserverTestCase):
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
events.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
room.register_deprecated_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor, clock, hs):
|
||||||
|
self.event_creation_handler = hs.get_event_creation_handler()
|
||||||
|
hs.config.user_consent_version = "1"
|
||||||
|
|
||||||
|
consent_uri_builder = Mock()
|
||||||
|
consent_uri_builder.build_user_consent_uri.return_value = "http://example.com"
|
||||||
|
self.event_creation_handler._consent_uri_builder = consent_uri_builder
|
||||||
|
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
|
||||||
|
self.admin_user = self.register_user("admin", "pass", admin=True)
|
||||||
|
self.admin_user_tok = self.login("admin", "pass")
|
||||||
|
|
||||||
|
self.other_user = self.register_user("user", "pass")
|
||||||
|
self.other_user_tok = self.login("user", "pass")
|
||||||
|
|
||||||
|
# Mark the admin user as having consented
|
||||||
|
self.get_success(self.store.user_set_consent_version(self.admin_user, "1"))
|
||||||
|
|
||||||
|
self.room_id = self.helper.create_room_as(
|
||||||
|
self.other_user, tok=self.other_user_tok
|
||||||
|
)
|
||||||
|
self.url = "/_synapse/admin/v1/rooms/%s/delete" % self.room_id
|
||||||
|
|
||||||
|
def test_requester_is_no_admin(self):
|
||||||
|
"""
|
||||||
|
If the user is not a server admin, an error 403 is returned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"POST", self.url, json.dumps({}), access_token=self.other_user_tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
|
||||||
|
self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
|
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
||||||
|
|
||||||
|
def test_room_does_not_exist(self):
|
||||||
|
"""
|
||||||
|
Check that unknown rooms/server return error 404.
|
||||||
|
"""
|
||||||
|
url = "/_synapse/admin/v1/rooms/!unknown:test/delete"
|
||||||
|
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"POST", url, json.dumps({}), access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
|
||||||
|
self.assertEqual(404, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
|
self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"])
|
||||||
|
|
||||||
|
def test_room_is_not_valid(self):
|
||||||
|
"""
|
||||||
|
Check that invalid room names, return an error 400.
|
||||||
|
"""
|
||||||
|
url = "/_synapse/admin/v1/rooms/invalidroom/delete"
|
||||||
|
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"POST", url, json.dumps({}), access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
|
self.assertEqual(
|
||||||
|
"invalidroom is not a legal room ID", channel.json_body["error"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_new_room_user_does_not_exist(self):
|
||||||
|
"""
|
||||||
|
Tests that the user ID must be from local server but it does not have to exist.
|
||||||
|
"""
|
||||||
|
body = json.dumps({"new_room_user_id": "@unknown:test"})
|
||||||
|
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.url,
|
||||||
|
content=body.encode(encoding="utf_8"),
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
|
||||||
|
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
|
self.assertIn("new_room_id", channel.json_body)
|
||||||
|
self.assertIn("kicked_users", channel.json_body)
|
||||||
|
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||||
|
self.assertIn("local_aliases", channel.json_body)
|
||||||
|
|
||||||
|
def test_new_room_user_is_not_local(self):
|
||||||
|
"""
|
||||||
|
Check that only local users can create new room to move members.
|
||||||
|
"""
|
||||||
|
body = json.dumps({"new_room_user_id": "@not:exist.bla"})
|
||||||
|
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.url,
|
||||||
|
content=body.encode(encoding="utf_8"),
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
|
self.assertEqual(
|
||||||
|
"User must be our own: @not:exist.bla", channel.json_body["error"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_block_is_not_bool(self):
|
||||||
|
"""
|
||||||
|
If parameter `block` is not boolean, return an error
|
||||||
|
"""
|
||||||
|
body = json.dumps({"block": "NotBool"})
|
||||||
|
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.url,
|
||||||
|
content=body.encode(encoding="utf_8"),
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
|
||||||
|
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
|
self.assertEqual(Codes.BAD_JSON, channel.json_body["errcode"])
|
||||||
|
|
||||||
|
def test_purge_room_and_block(self):
|
||||||
|
"""Test to purge a room and block it.
|
||||||
|
Members will not be moved to a new room and will not receive a message.
|
||||||
|
"""
|
||||||
|
# Test that room is not purged
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self._is_purged(self.room_id)
|
||||||
|
|
||||||
|
# Test that room is not blocked
|
||||||
|
self._is_blocked(self.room_id, expect=False)
|
||||||
|
|
||||||
|
# Assert one user in room
|
||||||
|
self._is_member(room_id=self.room_id, user_id=self.other_user)
|
||||||
|
|
||||||
|
body = json.dumps({"block": True})
|
||||||
|
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.url.encode("ascii"),
|
||||||
|
content=body.encode(encoding="utf_8"),
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
|
||||||
|
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
|
self.assertEqual(None, channel.json_body["new_room_id"])
|
||||||
|
self.assertEqual(self.other_user, channel.json_body["kicked_users"][0])
|
||||||
|
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||||
|
self.assertIn("local_aliases", channel.json_body)
|
||||||
|
|
||||||
|
self._is_purged(self.room_id)
|
||||||
|
self._is_blocked(self.room_id, expect=True)
|
||||||
|
self._has_no_members(self.room_id)
|
||||||
|
|
||||||
|
def test_purge_room_and_not_block(self):
|
||||||
|
"""Test to purge a room and do not block it.
|
||||||
|
Members will not be moved to a new room and will not receive a message.
|
||||||
|
"""
|
||||||
|
# Test that room is not purged
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self._is_purged(self.room_id)
|
||||||
|
|
||||||
|
# Test that room is not blocked
|
||||||
|
self._is_blocked(self.room_id, expect=False)
|
||||||
|
|
||||||
|
# Assert one user in room
|
||||||
|
self._is_member(room_id=self.room_id, user_id=self.other_user)
|
||||||
|
|
||||||
|
body = json.dumps({"block": False})
|
||||||
|
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.url.encode("ascii"),
|
||||||
|
content=body.encode(encoding="utf_8"),
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
|
||||||
|
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
|
self.assertEqual(None, channel.json_body["new_room_id"])
|
||||||
|
self.assertEqual(self.other_user, channel.json_body["kicked_users"][0])
|
||||||
|
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||||
|
self.assertIn("local_aliases", channel.json_body)
|
||||||
|
|
||||||
|
self._is_purged(self.room_id)
|
||||||
|
self._is_blocked(self.room_id, expect=False)
|
||||||
|
self._has_no_members(self.room_id)
|
||||||
|
|
||||||
|
def test_shutdown_room_consent(self):
|
||||||
|
"""Test that we can shutdown rooms with local users who have not
|
||||||
|
yet accepted the privacy policy. This used to fail when we tried to
|
||||||
|
force part the user from the old room.
|
||||||
|
Members will be moved to a new room and will receive a message.
|
||||||
|
"""
|
||||||
|
self.event_creation_handler._block_events_without_consent_error = None
|
||||||
|
|
||||||
|
# Assert one user in room
|
||||||
|
users_in_room = self.get_success(self.store.get_users_in_room(self.room_id))
|
||||||
|
self.assertEqual([self.other_user], users_in_room)
|
||||||
|
|
||||||
|
# Enable require consent to send events
|
||||||
|
self.event_creation_handler._block_events_without_consent_error = "Error"
|
||||||
|
|
||||||
|
# Assert that the user is getting consent error
|
||||||
|
self.helper.send(
|
||||||
|
self.room_id, body="foo", tok=self.other_user_tok, expect_code=403
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that room is not purged
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self._is_purged(self.room_id)
|
||||||
|
|
||||||
|
# Assert one user in room
|
||||||
|
self._is_member(room_id=self.room_id, user_id=self.other_user)
|
||||||
|
|
||||||
|
# Test that the admin can still send shutdown
|
||||||
|
url = "/_synapse/admin/v1/rooms/%s/delete" % self.room_id
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
url.encode("ascii"),
|
||||||
|
json.dumps({"new_room_user_id": self.admin_user}),
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
|
||||||
|
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
|
self.assertEqual(self.other_user, channel.json_body["kicked_users"][0])
|
||||||
|
self.assertIn("new_room_id", channel.json_body)
|
||||||
|
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||||
|
self.assertIn("local_aliases", channel.json_body)
|
||||||
|
|
||||||
|
# Test that member has moved to new room
|
||||||
|
self._is_member(
|
||||||
|
room_id=channel.json_body["new_room_id"], user_id=self.other_user
|
||||||
|
)
|
||||||
|
|
||||||
|
self._is_purged(self.room_id)
|
||||||
|
self._has_no_members(self.room_id)
|
||||||
|
|
||||||
|
def test_shutdown_room_block_peek(self):
|
||||||
|
"""Test that a world_readable room can no longer be peeked into after
|
||||||
|
it has been shut down.
|
||||||
|
Members will be moved to a new room and will receive a message.
|
||||||
|
"""
|
||||||
|
self.event_creation_handler._block_events_without_consent_error = None
|
||||||
|
|
||||||
|
# Enable world readable
|
||||||
|
url = "rooms/%s/state/m.room.history_visibility" % (self.room_id,)
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
url.encode("ascii"),
|
||||||
|
json.dumps({"history_visibility": "world_readable"}),
|
||||||
|
access_token=self.other_user_tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
|
|
||||||
|
# Test that room is not purged
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self._is_purged(self.room_id)
|
||||||
|
|
||||||
|
# Assert one user in room
|
||||||
|
self._is_member(room_id=self.room_id, user_id=self.other_user)
|
||||||
|
|
||||||
|
# Test that the admin can still send shutdown
|
||||||
|
url = "/_synapse/admin/v1/rooms/%s/delete" % self.room_id
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
url.encode("ascii"),
|
||||||
|
json.dumps({"new_room_user_id": self.admin_user}),
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
|
||||||
|
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
||||||
|
self.assertEqual(self.other_user, channel.json_body["kicked_users"][0])
|
||||||
|
self.assertIn("new_room_id", channel.json_body)
|
||||||
|
self.assertIn("failed_to_kick_users", channel.json_body)
|
||||||
|
self.assertIn("local_aliases", channel.json_body)
|
||||||
|
|
||||||
|
# Test that member has moved to new room
|
||||||
|
self._is_member(
|
||||||
|
room_id=channel.json_body["new_room_id"], user_id=self.other_user
|
||||||
|
)
|
||||||
|
|
||||||
|
self._is_purged(self.room_id)
|
||||||
|
self._has_no_members(self.room_id)
|
||||||
|
|
||||||
|
# Assert we can no longer peek into the room
|
||||||
|
self._assert_peek(self.room_id, expect_code=403)
|
||||||
|
|
||||||
|
def _is_blocked(self, room_id, expect=True):
|
||||||
|
"""Assert that the room is blocked or not
|
||||||
|
"""
|
||||||
|
d = self.store.is_room_blocked(room_id)
|
||||||
|
if expect:
|
||||||
|
self.assertTrue(self.get_success(d))
|
||||||
|
else:
|
||||||
|
self.assertIsNone(self.get_success(d))
|
||||||
|
|
||||||
|
def _has_no_members(self, room_id):
|
||||||
|
"""Assert there is now no longer anyone in the room
|
||||||
|
"""
|
||||||
|
users_in_room = self.get_success(self.store.get_users_in_room(room_id))
|
||||||
|
self.assertEqual([], users_in_room)
|
||||||
|
|
||||||
|
def _is_member(self, room_id, user_id):
|
||||||
|
"""Test that user is member of the room
|
||||||
|
"""
|
||||||
|
users_in_room = self.get_success(self.store.get_users_in_room(room_id))
|
||||||
|
self.assertIn(user_id, users_in_room)
|
||||||
|
|
||||||
|
def _is_purged(self, room_id):
|
||||||
|
"""Test that the following tables have been purged of all rows related to the room.
|
||||||
|
"""
|
||||||
|
for table in (
|
||||||
|
"current_state_events",
|
||||||
|
"event_backward_extremities",
|
||||||
|
"event_forward_extremities",
|
||||||
|
"event_json",
|
||||||
|
"event_push_actions",
|
||||||
|
"event_search",
|
||||||
|
"events",
|
||||||
|
"group_rooms",
|
||||||
|
"public_room_list_stream",
|
||||||
|
"receipts_graph",
|
||||||
|
"receipts_linearized",
|
||||||
|
"room_aliases",
|
||||||
|
"room_depth",
|
||||||
|
"room_memberships",
|
||||||
|
"room_stats_state",
|
||||||
|
"room_stats_current",
|
||||||
|
"room_stats_historical",
|
||||||
|
"room_stats_earliest_token",
|
||||||
|
"rooms",
|
||||||
|
"stream_ordering_to_exterm",
|
||||||
|
"users_in_public_rooms",
|
||||||
|
"users_who_share_private_rooms",
|
||||||
|
"appservice_room_list",
|
||||||
|
"e2e_room_keys",
|
||||||
|
"event_push_summary",
|
||||||
|
"pusher_throttle",
|
||||||
|
"group_summary_rooms",
|
||||||
|
"local_invites",
|
||||||
|
"room_account_data",
|
||||||
|
"room_tags",
|
||||||
|
# "state_groups", # Current impl leaves orphaned state groups around.
|
||||||
|
"state_groups_state",
|
||||||
|
):
|
||||||
|
count = self.get_success(
|
||||||
|
self.store.db.simple_select_one_onecol(
|
||||||
|
table=table,
|
||||||
|
keyvalues={"room_id": room_id},
|
||||||
|
retcol="COUNT(*)",
|
||||||
|
desc="test_purge_room",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(count, 0, msg="Rows not purged in {}".format(table))
|
||||||
|
|
||||||
|
def _assert_peek(self, room_id, expect_code):
|
||||||
|
"""Assert that the admin user can (or cannot) peek into the room.
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = "rooms/%s/initialSync" % (room_id,)
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"GET", url.encode("ascii"), access_token=self.admin_user_tok
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
self.assertEqual(
|
||||||
|
expect_code, int(channel.result["code"]), msg=channel.result["body"]
|
||||||
|
)
|
||||||
|
|
||||||
|
url = "events?timeout=0&room_id=" + room_id
|
||||||
|
request, channel = self.make_request(
|
||||||
|
"GET", url.encode("ascii"), access_token=self.admin_user_tok
|
||||||
|
)
|
||||||
|
self.render(request)
|
||||||
|
self.assertEqual(
|
||||||
|
expect_code, int(channel.result["code"]), msg=channel.result["body"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PurgeRoomTestCase(unittest.HomeserverTestCase):
|
class PurgeRoomTestCase(unittest.HomeserverTestCase):
|
||||||
"""Test /purge_room admin API.
|
"""Test /purge_room admin API.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -55,6 +55,10 @@ class RoomStoreTestCase(unittest.TestCase):
|
||||||
(yield self.store.get_room(self.room.to_string())),
|
(yield self.store.get_room(self.room.to_string())),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_get_room_unknown_room(self):
|
||||||
|
self.assertIsNone((yield self.store.get_room("!uknown:test")),)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_get_room_with_stats(self):
|
def test_get_room_with_stats(self):
|
||||||
self.assertDictContainsSubset(
|
self.assertDictContainsSubset(
|
||||||
|
@ -66,6 +70,10 @@ class RoomStoreTestCase(unittest.TestCase):
|
||||||
(yield self.store.get_room_with_stats(self.room.to_string())),
|
(yield self.store.get_room_with_stats(self.room.to_string())),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_get_room_with_stats_unknown_room(self):
|
||||||
|
self.assertIsNone((yield self.store.get_room_with_stats("!uknown:test")),)
|
||||||
|
|
||||||
|
|
||||||
class RoomEventsStoreTestCase(unittest.TestCase):
|
class RoomEventsStoreTestCase(unittest.TestCase):
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
|
Loading…
Reference in New Issue