mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-01-30 14:03:25 -05:00
Convert delete room admin API to async endpoint (#11223)
Signed-off-by: Dirk Klimpel dirk@klimpel.org
This commit is contained in:
parent
c99da2d079
commit
8840a7b7f1
1
changelog.d/11223.feature
Normal file
1
changelog.d/11223.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add a new version of delete room admin API `DELETE /_synapse/admin/v2/rooms/<room_id>` to run it in background. Contributed by @dklimpel.
|
@ -70,6 +70,8 @@ This API returns a JSON body like the following:
|
|||||||
|
|
||||||
The status will be one of `active`, `complete`, or `failed`.
|
The status will be one of `active`, `complete`, or `failed`.
|
||||||
|
|
||||||
|
If `status` is `failed` there will be a string `error` with the error message.
|
||||||
|
|
||||||
## Reclaim disk space (Postgres)
|
## Reclaim disk space (Postgres)
|
||||||
|
|
||||||
To reclaim the disk space and return it to the operating system, you need to run
|
To reclaim the disk space and return it to the operating system, you need to run
|
||||||
|
@ -4,6 +4,9 @@
|
|||||||
- [Room Members API](#room-members-api)
|
- [Room Members API](#room-members-api)
|
||||||
- [Room State API](#room-state-api)
|
- [Room State API](#room-state-api)
|
||||||
- [Delete Room API](#delete-room-api)
|
- [Delete Room API](#delete-room-api)
|
||||||
|
* [Version 1 (old version)](#version-1-old-version)
|
||||||
|
* [Version 2 (new version)](#version-2-new-version)
|
||||||
|
* [Status of deleting rooms](#status-of-deleting-rooms)
|
||||||
* [Undoing room shutdowns](#undoing-room-shutdowns)
|
* [Undoing room shutdowns](#undoing-room-shutdowns)
|
||||||
- [Make Room Admin API](#make-room-admin-api)
|
- [Make Room Admin API](#make-room-admin-api)
|
||||||
- [Forward Extremities Admin API](#forward-extremities-admin-api)
|
- [Forward Extremities Admin API](#forward-extremities-admin-api)
|
||||||
@ -397,10 +400,10 @@ as room administrator and will contain a message explaining what happened. Users
|
|||||||
to the new room will have power level `-10` by default, and thus be unable to speak.
|
to the new room will have power level `-10` by default, and thus be unable to speak.
|
||||||
|
|
||||||
If `block` is `true`, users will be prevented from joining the old room.
|
If `block` is `true`, users will be prevented from joining the old room.
|
||||||
This option can also be used to pre-emptively block a room, even if it's unknown
|
This option can in [Version 1](#version-1-old-version) also be used to pre-emptively
|
||||||
to this homeserver. In this case, the room will be blocked, and no further action
|
block a room, even if it's unknown to this homeserver. In this case, the room will be
|
||||||
will be taken. If `block` is `false`, attempting to delete an unknown room is
|
blocked, and no further action will be taken. If `block` is `false`, attempting to
|
||||||
invalid and will be rejected as a bad request.
|
delete an unknown room is invalid and will be rejected as a bad request.
|
||||||
|
|
||||||
This API will remove all trace of the old room from your database after removing
|
This API will remove all trace of the old room from your database after removing
|
||||||
all local users. If `purge` is `true` (the default), all traces of the old room will
|
all local users. If `purge` is `true` (the default), all traces of the old room will
|
||||||
@ -412,6 +415,17 @@ several minutes or longer.
|
|||||||
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.
|
||||||
|
|
||||||
|
To use it, you will need to authenticate by providing an ``access_token`` for a
|
||||||
|
server admin: see [Admin API](../usage/administration/admin_api).
|
||||||
|
|
||||||
|
## Version 1 (old version)
|
||||||
|
|
||||||
|
This version works synchronously. That means you only get the response once the server has
|
||||||
|
finished the action, which may take a long time. If you request the same action
|
||||||
|
a second time, and the server has not finished the first one, the second request will block.
|
||||||
|
This is fixed in version 2 of this API. The parameters are the same in both APIs.
|
||||||
|
This API will become deprecated in the future.
|
||||||
|
|
||||||
The API is:
|
The API is:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -430,9 +444,6 @@ with a body of:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
To use it, you will need to authenticate by providing an ``access_token`` for a
|
|
||||||
server admin: see [Admin API](../usage/administration/admin_api).
|
|
||||||
|
|
||||||
A response body like the following is returned:
|
A response body like the following is returned:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -449,6 +460,44 @@ A response body like the following is returned:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The parameters and response values have the same format as
|
||||||
|
[version 2](#version-2-new-version) of the API.
|
||||||
|
|
||||||
|
## Version 2 (new version)
|
||||||
|
|
||||||
|
**Note**: This API is new, experimental and "subject to change".
|
||||||
|
|
||||||
|
This version works asynchronously, meaning you get the response from server immediately
|
||||||
|
while the server works on that task in background. You can then request the status of the action
|
||||||
|
to check if it has completed.
|
||||||
|
|
||||||
|
The API is:
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /_synapse/admin/v2/rooms/<room_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
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,
|
||||||
|
"purge": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The API starts the shut down and purge running, and returns immediately with a JSON body with
|
||||||
|
a purge id:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"delete_id": "<opaque id>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
The following parameters should be set in the URL:
|
The following parameters should be set in the URL:
|
||||||
@ -470,7 +519,8 @@ The following JSON body parameters are available:
|
|||||||
is not permitted and rooms in violation will be blocked.`
|
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,
|
* `block` - Optional. If set to `true`, this room will be added to a blocking list,
|
||||||
preventing future attempts to join the room. Rooms can be blocked
|
preventing future attempts to join the room. Rooms can be blocked
|
||||||
even if they're not yet known to the homeserver. Defaults to `false`.
|
even if they're not yet known to the homeserver (only with
|
||||||
|
[Version 1](#version-1-old-version) of the API). Defaults to `false`.
|
||||||
* `purge` - Optional. If set to `true`, it will remove all traces of the room from your database.
|
* `purge` - Optional. If set to `true`, it will remove all traces of the room from your database.
|
||||||
Defaults to `true`.
|
Defaults to `true`.
|
||||||
* `force_purge` - Optional, and ignored unless `purge` is `true`. If set to `true`, it
|
* `force_purge` - Optional, and ignored unless `purge` is `true`. If set to `true`, it
|
||||||
@ -480,17 +530,124 @@ The following JSON body parameters are available:
|
|||||||
|
|
||||||
The JSON body must not be empty. The body must be at least `{}`.
|
The JSON body must not be empty. The body must be at least `{}`.
|
||||||
|
|
||||||
**Response**
|
## Status of deleting rooms
|
||||||
|
|
||||||
|
**Note**: This API is new, experimental and "subject to change".
|
||||||
|
|
||||||
|
It is possible to query the status of the background task for deleting rooms.
|
||||||
|
The status can be queried up to 24 hours after completion of the task,
|
||||||
|
or until Synapse is restarted (whichever happens first).
|
||||||
|
|
||||||
|
### Query by `room_id`
|
||||||
|
|
||||||
|
With this API you can get the status of all active deletion tasks, and all those completed in the last 24h,
|
||||||
|
for the given `room_id`.
|
||||||
|
|
||||||
|
The API is:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /_synapse/admin/v2/rooms/<room_id>/delete_status
|
||||||
|
```
|
||||||
|
|
||||||
|
A response body like the following is returned:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"delete_id": "delete_id1",
|
||||||
|
"status": "failed",
|
||||||
|
"error": "error message",
|
||||||
|
"shutdown_room": {
|
||||||
|
"kicked_users": [],
|
||||||
|
"failed_to_kick_users": [],
|
||||||
|
"local_aliases": [],
|
||||||
|
"new_room_id": null
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"delete_id": "delete_id2",
|
||||||
|
"status": "purging",
|
||||||
|
"shutdown_room": {
|
||||||
|
"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.
|
||||||
|
|
||||||
|
### Query by `delete_id`
|
||||||
|
|
||||||
|
With this API you can get the status of one specific task by `delete_id`.
|
||||||
|
|
||||||
|
The API is:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /_synapse/admin/v2/rooms/delete_status/<delete_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
A response body like the following is returned:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "purging",
|
||||||
|
"shutdown_room": {
|
||||||
|
"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:
|
||||||
|
|
||||||
|
* `delete_id` - The ID for this delete.
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
The following fields are returned in the JSON response body:
|
The following fields are returned in the JSON response body:
|
||||||
|
|
||||||
* `kicked_users` - An array of users (`user_id`) that were kicked.
|
- `results` - An array of objects, each containing information about one task.
|
||||||
* `failed_to_kick_users` - An array of users (`user_id`) that that were not kicked.
|
This field is omitted from the result when you query by `delete_id`.
|
||||||
* `local_aliases` - An array of strings representing the local aliases that were migrated from
|
Task objects contain the following fields:
|
||||||
the old room to the new.
|
- `delete_id` - The ID for this purge if you query by `room_id`.
|
||||||
* `new_room_id` - A string representing the room ID of the new room, or `null` if
|
- `status` - The status will be one of:
|
||||||
no such room was created.
|
- `shutting_down` - The process is removing users from the room.
|
||||||
|
- `purging` - The process is purging the room and event data from database.
|
||||||
|
- `complete` - The process has completed successfully.
|
||||||
|
- `failed` - The process is aborted, an error has occurred.
|
||||||
|
- `error` - A string that shows an error message if `status` is `failed`.
|
||||||
|
Otherwise this field is hidden.
|
||||||
|
- `shutdown_room` - An object containing information about the result of shutting down the room.
|
||||||
|
*Note:* The result is shown after removing the room members.
|
||||||
|
The delete process can still be running. Please pay attention to the `status`.
|
||||||
|
- `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, or `null` if
|
||||||
|
no such room was created.
|
||||||
|
|
||||||
## Undoing room deletions
|
## Undoing room deletions
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
# 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 typing import TYPE_CHECKING, Any, Dict, Optional, Set
|
from typing import TYPE_CHECKING, Any, Collection, Dict, List, Optional, Set
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ from twisted.python.failure import Failure
|
|||||||
from synapse.api.constants import EventTypes, Membership
|
from synapse.api.constants import EventTypes, Membership
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from synapse.api.filtering import Filter
|
from synapse.api.filtering import Filter
|
||||||
from synapse.logging.context import run_in_background
|
from synapse.handlers.room import ShutdownRoomResponse
|
||||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||||
from synapse.storage.state import StateFilter
|
from synapse.storage.state import StateFilter
|
||||||
from synapse.streams.config import PaginationConfig
|
from synapse.streams.config import PaginationConfig
|
||||||
@ -56,11 +56,62 @@ class PurgeStatus:
|
|||||||
STATUS_FAILED: "failed",
|
STATUS_FAILED: "failed",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Save the error message if an error occurs
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
# Tracks whether this request has completed. One of STATUS_{ACTIVE,COMPLETE,FAILED}.
|
# Tracks whether this request has completed. One of STATUS_{ACTIVE,COMPLETE,FAILED}.
|
||||||
status: int = STATUS_ACTIVE
|
status: int = STATUS_ACTIVE
|
||||||
|
|
||||||
def asdict(self) -> JsonDict:
|
def asdict(self) -> JsonDict:
|
||||||
return {"status": PurgeStatus.STATUS_TEXT[self.status]}
|
ret = {"status": PurgeStatus.STATUS_TEXT[self.status]}
|
||||||
|
if self.error:
|
||||||
|
ret["error"] = self.error
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True, auto_attribs=True)
|
||||||
|
class DeleteStatus:
|
||||||
|
"""Object tracking the status of a delete room request
|
||||||
|
|
||||||
|
This class contains information on the progress of a delete room request, for
|
||||||
|
return by get_delete_status.
|
||||||
|
"""
|
||||||
|
|
||||||
|
STATUS_PURGING = 0
|
||||||
|
STATUS_COMPLETE = 1
|
||||||
|
STATUS_FAILED = 2
|
||||||
|
STATUS_SHUTTING_DOWN = 3
|
||||||
|
|
||||||
|
STATUS_TEXT = {
|
||||||
|
STATUS_PURGING: "purging",
|
||||||
|
STATUS_COMPLETE: "complete",
|
||||||
|
STATUS_FAILED: "failed",
|
||||||
|
STATUS_SHUTTING_DOWN: "shutting_down",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tracks whether this request has completed.
|
||||||
|
# One of STATUS_{PURGING,COMPLETE,FAILED,SHUTTING_DOWN}.
|
||||||
|
status: int = STATUS_PURGING
|
||||||
|
|
||||||
|
# Save the error message if an error occurs
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
# Saves the result of an action to give it back to REST API
|
||||||
|
shutdown_room: ShutdownRoomResponse = {
|
||||||
|
"kicked_users": [],
|
||||||
|
"failed_to_kick_users": [],
|
||||||
|
"local_aliases": [],
|
||||||
|
"new_room_id": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def asdict(self) -> JsonDict:
|
||||||
|
ret = {
|
||||||
|
"status": DeleteStatus.STATUS_TEXT[self.status],
|
||||||
|
"shutdown_room": self.shutdown_room,
|
||||||
|
}
|
||||||
|
if self.error:
|
||||||
|
ret["error"] = self.error
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class PaginationHandler:
|
class PaginationHandler:
|
||||||
@ -70,6 +121,9 @@ class PaginationHandler:
|
|||||||
paginating during a purge.
|
paginating during a purge.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# when to remove a completed deletion/purge from the results map
|
||||||
|
CLEAR_PURGE_AFTER_MS = 1000 * 3600 * 24 # 24 hours
|
||||||
|
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
@ -78,11 +132,18 @@ class PaginationHandler:
|
|||||||
self.state_store = self.storage.state
|
self.state_store = self.storage.state
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self._server_name = hs.hostname
|
self._server_name = hs.hostname
|
||||||
|
self._room_shutdown_handler = hs.get_room_shutdown_handler()
|
||||||
|
|
||||||
self.pagination_lock = ReadWriteLock()
|
self.pagination_lock = ReadWriteLock()
|
||||||
|
# IDs of rooms in which there currently an active purge *or delete* operation.
|
||||||
self._purges_in_progress_by_room: Set[str] = set()
|
self._purges_in_progress_by_room: Set[str] = set()
|
||||||
# map from purge id to PurgeStatus
|
# map from purge id to PurgeStatus
|
||||||
self._purges_by_id: Dict[str, PurgeStatus] = {}
|
self._purges_by_id: Dict[str, PurgeStatus] = {}
|
||||||
|
# map from purge id to DeleteStatus
|
||||||
|
self._delete_by_id: Dict[str, DeleteStatus] = {}
|
||||||
|
# map from room id to delete ids
|
||||||
|
# Dict[`room_id`, List[`delete_id`]]
|
||||||
|
self._delete_by_room: Dict[str, List[str]] = {}
|
||||||
self._event_serializer = hs.get_event_client_serializer()
|
self._event_serializer = hs.get_event_client_serializer()
|
||||||
|
|
||||||
self._retention_default_max_lifetime = (
|
self._retention_default_max_lifetime = (
|
||||||
@ -265,8 +326,13 @@ class PaginationHandler:
|
|||||||
logger.info("[purge] starting purge_id %s", purge_id)
|
logger.info("[purge] starting purge_id %s", purge_id)
|
||||||
|
|
||||||
self._purges_by_id[purge_id] = PurgeStatus()
|
self._purges_by_id[purge_id] = PurgeStatus()
|
||||||
run_in_background(
|
run_as_background_process(
|
||||||
self._purge_history, purge_id, room_id, token, delete_local_events
|
"purge_history",
|
||||||
|
self._purge_history,
|
||||||
|
purge_id,
|
||||||
|
room_id,
|
||||||
|
token,
|
||||||
|
delete_local_events,
|
||||||
)
|
)
|
||||||
return purge_id
|
return purge_id
|
||||||
|
|
||||||
@ -276,7 +342,7 @@ class PaginationHandler:
|
|||||||
"""Carry out a history purge on a room.
|
"""Carry out a history purge on a room.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
purge_id: The id for this purge
|
purge_id: The ID for this purge.
|
||||||
room_id: The room to purge from
|
room_id: The room to purge from
|
||||||
token: topological token to delete events before
|
token: topological token to delete events before
|
||||||
delete_local_events: True to delete local events as well as remote ones
|
delete_local_events: True to delete local events as well as remote ones
|
||||||
@ -295,6 +361,7 @@ class PaginationHandler:
|
|||||||
"[purge] failed", exc_info=(f.type, f.value, f.getTracebackObject()) # type: ignore
|
"[purge] failed", exc_info=(f.type, f.value, f.getTracebackObject()) # type: ignore
|
||||||
)
|
)
|
||||||
self._purges_by_id[purge_id].status = PurgeStatus.STATUS_FAILED
|
self._purges_by_id[purge_id].status = PurgeStatus.STATUS_FAILED
|
||||||
|
self._purges_by_id[purge_id].error = f.getErrorMessage()
|
||||||
finally:
|
finally:
|
||||||
self._purges_in_progress_by_room.discard(room_id)
|
self._purges_in_progress_by_room.discard(room_id)
|
||||||
|
|
||||||
@ -302,7 +369,9 @@ class PaginationHandler:
|
|||||||
def clear_purge() -> None:
|
def clear_purge() -> None:
|
||||||
del self._purges_by_id[purge_id]
|
del self._purges_by_id[purge_id]
|
||||||
|
|
||||||
self.hs.get_reactor().callLater(24 * 3600, clear_purge)
|
self.hs.get_reactor().callLater(
|
||||||
|
PaginationHandler.CLEAR_PURGE_AFTER_MS / 1000, clear_purge
|
||||||
|
)
|
||||||
|
|
||||||
def get_purge_status(self, purge_id: str) -> Optional[PurgeStatus]:
|
def get_purge_status(self, purge_id: str) -> Optional[PurgeStatus]:
|
||||||
"""Get the current status of an active purge
|
"""Get the current status of an active purge
|
||||||
@ -312,8 +381,25 @@ class PaginationHandler:
|
|||||||
"""
|
"""
|
||||||
return self._purges_by_id.get(purge_id)
|
return self._purges_by_id.get(purge_id)
|
||||||
|
|
||||||
|
def get_delete_status(self, delete_id: str) -> Optional[DeleteStatus]:
|
||||||
|
"""Get the current status of an active deleting
|
||||||
|
|
||||||
|
Args:
|
||||||
|
delete_id: delete_id returned by start_shutdown_and_purge_room
|
||||||
|
"""
|
||||||
|
return self._delete_by_id.get(delete_id)
|
||||||
|
|
||||||
|
def get_delete_ids_by_room(self, room_id: str) -> Optional[Collection[str]]:
|
||||||
|
"""Get all active delete ids by room
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room_id: room_id that is deleted
|
||||||
|
"""
|
||||||
|
return self._delete_by_room.get(room_id)
|
||||||
|
|
||||||
async def purge_room(self, room_id: str, force: bool = False) -> None:
|
async def purge_room(self, room_id: str, force: bool = False) -> None:
|
||||||
"""Purge the given room from the database.
|
"""Purge the given room from the database.
|
||||||
|
This function is part the delete room v1 API.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
room_id: room to be purged
|
room_id: room to be purged
|
||||||
@ -472,3 +558,192 @@ class PaginationHandler:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return chunk
|
return chunk
|
||||||
|
|
||||||
|
async def _shutdown_and_purge_room(
|
||||||
|
self,
|
||||||
|
delete_id: str,
|
||||||
|
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,
|
||||||
|
purge: bool = True,
|
||||||
|
force_purge: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Shuts down and purges a room.
|
||||||
|
|
||||||
|
See `RoomShutdownHandler.shutdown_room` for details of creation of the new room
|
||||||
|
|
||||||
|
Args:
|
||||||
|
delete_id: The ID for this delete.
|
||||||
|
room_id: The ID of the room to shut down.
|
||||||
|
requester_user_id:
|
||||||
|
User who requested the action. Will be recorded as putting 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`.
|
||||||
|
purge:
|
||||||
|
If set to `true`, purge the given room from the database.
|
||||||
|
force_purge:
|
||||||
|
If set to `true`, the room will be purged from database
|
||||||
|
also if it fails to remove some users from room.
|
||||||
|
|
||||||
|
Saves a `RoomShutdownHandler.ShutdownRoomResponse` in `DeleteStatus`:
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._purges_in_progress_by_room.add(room_id)
|
||||||
|
try:
|
||||||
|
with await self.pagination_lock.write(room_id):
|
||||||
|
self._delete_by_id[delete_id].status = DeleteStatus.STATUS_SHUTTING_DOWN
|
||||||
|
self._delete_by_id[
|
||||||
|
delete_id
|
||||||
|
].shutdown_room = await self._room_shutdown_handler.shutdown_room(
|
||||||
|
room_id=room_id,
|
||||||
|
requester_user_id=requester_user_id,
|
||||||
|
new_room_user_id=new_room_user_id,
|
||||||
|
new_room_name=new_room_name,
|
||||||
|
message=message,
|
||||||
|
block=block,
|
||||||
|
)
|
||||||
|
self._delete_by_id[delete_id].status = DeleteStatus.STATUS_PURGING
|
||||||
|
|
||||||
|
if purge:
|
||||||
|
logger.info("starting purge room_id %s", room_id)
|
||||||
|
|
||||||
|
# first check that we have no users in this room
|
||||||
|
if not force_purge:
|
||||||
|
joined = await self.store.is_host_joined(
|
||||||
|
room_id, self._server_name
|
||||||
|
)
|
||||||
|
if joined:
|
||||||
|
raise SynapseError(
|
||||||
|
400, "Users are still joined to this room"
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.storage.purge_events.purge_room(room_id)
|
||||||
|
|
||||||
|
logger.info("complete")
|
||||||
|
self._delete_by_id[delete_id].status = DeleteStatus.STATUS_COMPLETE
|
||||||
|
except Exception:
|
||||||
|
f = Failure()
|
||||||
|
logger.error(
|
||||||
|
"failed",
|
||||||
|
exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore
|
||||||
|
)
|
||||||
|
self._delete_by_id[delete_id].status = DeleteStatus.STATUS_FAILED
|
||||||
|
self._delete_by_id[delete_id].error = f.getErrorMessage()
|
||||||
|
finally:
|
||||||
|
self._purges_in_progress_by_room.discard(room_id)
|
||||||
|
|
||||||
|
# remove the delete from the list 24 hours after it completes
|
||||||
|
def clear_delete() -> None:
|
||||||
|
del self._delete_by_id[delete_id]
|
||||||
|
self._delete_by_room[room_id].remove(delete_id)
|
||||||
|
if not self._delete_by_room[room_id]:
|
||||||
|
del self._delete_by_room[room_id]
|
||||||
|
|
||||||
|
self.hs.get_reactor().callLater(
|
||||||
|
PaginationHandler.CLEAR_PURGE_AFTER_MS / 1000, clear_delete
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_shutdown_and_purge_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,
|
||||||
|
purge: bool = True,
|
||||||
|
force_purge: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""Start off shut down and purge on a room.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
purge:
|
||||||
|
If set to `true`, purge the given room from the database.
|
||||||
|
force_purge:
|
||||||
|
If set to `true`, the room will be purged from database
|
||||||
|
also if it fails to remove some users from room.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
unique ID for this delete transaction.
|
||||||
|
"""
|
||||||
|
if room_id in self._purges_in_progress_by_room:
|
||||||
|
raise SynapseError(
|
||||||
|
400, "History purge already in progress for %s" % (room_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
# This check is double to `RoomShutdownHandler.shutdown_room`
|
||||||
|
# But here the requester get a direct response / error with HTTP request
|
||||||
|
# and do not have to check the purge status
|
||||||
|
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,)
|
||||||
|
)
|
||||||
|
|
||||||
|
delete_id = random_string(16)
|
||||||
|
|
||||||
|
# we log the delete_id here so that it can be tied back to the
|
||||||
|
# request id in the log lines.
|
||||||
|
logger.info(
|
||||||
|
"starting shutdown room_id %s with delete_id %s",
|
||||||
|
room_id,
|
||||||
|
delete_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._delete_by_id[delete_id] = DeleteStatus()
|
||||||
|
self._delete_by_room.setdefault(room_id, []).append(delete_id)
|
||||||
|
run_as_background_process(
|
||||||
|
"shutdown_and_purge_room",
|
||||||
|
self._shutdown_and_purge_room,
|
||||||
|
delete_id,
|
||||||
|
room_id,
|
||||||
|
requester_user_id,
|
||||||
|
new_room_user_id,
|
||||||
|
new_room_name,
|
||||||
|
message,
|
||||||
|
block,
|
||||||
|
purge,
|
||||||
|
force_purge,
|
||||||
|
)
|
||||||
|
return delete_id
|
||||||
|
@ -1279,6 +1279,17 @@ class RoomEventSource(EventSource[RoomStreamToken, EventBase]):
|
|||||||
|
|
||||||
|
|
||||||
class ShutdownRoomResponse(TypedDict):
|
class ShutdownRoomResponse(TypedDict):
|
||||||
|
"""
|
||||||
|
Attributes:
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
kicked_users: List[str]
|
kicked_users: List[str]
|
||||||
failed_to_kick_users: List[str]
|
failed_to_kick_users: List[str]
|
||||||
local_aliases: List[str]
|
local_aliases: List[str]
|
||||||
@ -1286,7 +1297,6 @@ class ShutdownRoomResponse(TypedDict):
|
|||||||
|
|
||||||
|
|
||||||
class RoomShutdownHandler:
|
class RoomShutdownHandler:
|
||||||
|
|
||||||
DEFAULT_MESSAGE = (
|
DEFAULT_MESSAGE = (
|
||||||
"Sharing illegal content on this server is not permitted and rooms in"
|
"Sharing illegal content on this server is not permitted and rooms in"
|
||||||
" violation will be blocked."
|
" violation will be blocked."
|
||||||
@ -1299,7 +1309,6 @@ class RoomShutdownHandler:
|
|||||||
self._room_creation_handler = hs.get_room_creation_handler()
|
self._room_creation_handler = hs.get_room_creation_handler()
|
||||||
self._replication = hs.get_replication_data_handler()
|
self._replication = hs.get_replication_data_handler()
|
||||||
self.event_creation_handler = hs.get_event_creation_handler()
|
self.event_creation_handler = hs.get_event_creation_handler()
|
||||||
self.state = hs.get_state_handler()
|
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
|
|
||||||
async def shutdown_room(
|
async def shutdown_room(
|
||||||
|
@ -46,6 +46,8 @@ from synapse.rest.admin.registration_tokens import (
|
|||||||
RegistrationTokenRestServlet,
|
RegistrationTokenRestServlet,
|
||||||
)
|
)
|
||||||
from synapse.rest.admin.rooms import (
|
from synapse.rest.admin.rooms import (
|
||||||
|
DeleteRoomStatusByDeleteIdRestServlet,
|
||||||
|
DeleteRoomStatusByRoomIdRestServlet,
|
||||||
ForwardExtremitiesRestServlet,
|
ForwardExtremitiesRestServlet,
|
||||||
JoinRoomAliasServlet,
|
JoinRoomAliasServlet,
|
||||||
ListRoomRestServlet,
|
ListRoomRestServlet,
|
||||||
@ -53,6 +55,7 @@ from synapse.rest.admin.rooms import (
|
|||||||
RoomEventContextServlet,
|
RoomEventContextServlet,
|
||||||
RoomMembersRestServlet,
|
RoomMembersRestServlet,
|
||||||
RoomRestServlet,
|
RoomRestServlet,
|
||||||
|
RoomRestV2Servlet,
|
||||||
RoomStateRestServlet,
|
RoomStateRestServlet,
|
||||||
)
|
)
|
||||||
from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
|
from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
|
||||||
@ -223,7 +226,10 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
|||||||
ListRoomRestServlet(hs).register(http_server)
|
ListRoomRestServlet(hs).register(http_server)
|
||||||
RoomStateRestServlet(hs).register(http_server)
|
RoomStateRestServlet(hs).register(http_server)
|
||||||
RoomRestServlet(hs).register(http_server)
|
RoomRestServlet(hs).register(http_server)
|
||||||
|
RoomRestV2Servlet(hs).register(http_server)
|
||||||
RoomMembersRestServlet(hs).register(http_server)
|
RoomMembersRestServlet(hs).register(http_server)
|
||||||
|
DeleteRoomStatusByDeleteIdRestServlet(hs).register(http_server)
|
||||||
|
DeleteRoomStatusByRoomIdRestServlet(hs).register(http_server)
|
||||||
JoinRoomAliasServlet(hs).register(http_server)
|
JoinRoomAliasServlet(hs).register(http_server)
|
||||||
VersionServlet(hs).register(http_server)
|
VersionServlet(hs).register(http_server)
|
||||||
UserAdminServlet(hs).register(http_server)
|
UserAdminServlet(hs).register(http_server)
|
||||||
|
@ -34,7 +34,7 @@ from synapse.rest.admin._base import (
|
|||||||
assert_user_is_admin,
|
assert_user_is_admin,
|
||||||
)
|
)
|
||||||
from synapse.storage.databases.main.room import RoomSortOrder
|
from synapse.storage.databases.main.room import RoomSortOrder
|
||||||
from synapse.types import JsonDict, UserID, create_requester
|
from synapse.types import JsonDict, RoomID, UserID, create_requester
|
||||||
from synapse.util import json_decoder
|
from synapse.util import json_decoder
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -46,6 +46,138 @@ if TYPE_CHECKING:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RoomRestV2Servlet(RestServlet):
|
||||||
|
"""Delete a room from server asynchronously with a background task.
|
||||||
|
|
||||||
|
It is a combination and improvement of shutdown 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.
|
||||||
|
|
||||||
|
If 'purge' is true, it will remove all traces of a room from the database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)$", "v2")
|
||||||
|
|
||||||
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
self._auth = hs.get_auth()
|
||||||
|
self._store = hs.get_datastore()
|
||||||
|
self._pagination_handler = hs.get_pagination_handler()
|
||||||
|
|
||||||
|
async def on_DELETE(
|
||||||
|
self, request: SynapseRequest, room_id: str
|
||||||
|
) -> Tuple[int, JsonDict]:
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
purge = content.get("purge", True)
|
||||||
|
if not isinstance(purge, bool):
|
||||||
|
raise SynapseError(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
"Param 'purge' must be a boolean, if given",
|
||||||
|
Codes.BAD_JSON,
|
||||||
|
)
|
||||||
|
|
||||||
|
force_purge = content.get("force_purge", False)
|
||||||
|
if not isinstance(force_purge, bool):
|
||||||
|
raise SynapseError(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
"Param 'force_purge' must be a boolean, if given",
|
||||||
|
Codes.BAD_JSON,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,))
|
||||||
|
|
||||||
|
delete_id = self._pagination_handler.start_shutdown_and_purge_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,
|
||||||
|
purge=purge,
|
||||||
|
force_purge=force_purge,
|
||||||
|
)
|
||||||
|
|
||||||
|
return 200, {"delete_id": delete_id}
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteRoomStatusByRoomIdRestServlet(RestServlet):
|
||||||
|
"""Get the status of the delete room background task."""
|
||||||
|
|
||||||
|
PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)/delete_status$", "v2")
|
||||||
|
|
||||||
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
self._auth = hs.get_auth()
|
||||||
|
self._pagination_handler = hs.get_pagination_handler()
|
||||||
|
|
||||||
|
async def on_GET(
|
||||||
|
self, request: SynapseRequest, room_id: str
|
||||||
|
) -> Tuple[int, JsonDict]:
|
||||||
|
|
||||||
|
await assert_requester_is_admin(self._auth, request)
|
||||||
|
|
||||||
|
if not RoomID.is_valid(room_id):
|
||||||
|
raise SynapseError(400, "%s is not a legal room ID" % (room_id,))
|
||||||
|
|
||||||
|
delete_ids = self._pagination_handler.get_delete_ids_by_room(room_id)
|
||||||
|
if delete_ids is None:
|
||||||
|
raise NotFoundError("No delete task for room_id '%s' found" % room_id)
|
||||||
|
|
||||||
|
response = []
|
||||||
|
for delete_id in delete_ids:
|
||||||
|
delete = self._pagination_handler.get_delete_status(delete_id)
|
||||||
|
if delete:
|
||||||
|
response += [
|
||||||
|
{
|
||||||
|
"delete_id": delete_id,
|
||||||
|
**delete.asdict(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return 200, {"results": cast(JsonDict, response)}
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteRoomStatusByDeleteIdRestServlet(RestServlet):
|
||||||
|
"""Get the status of the delete room background task."""
|
||||||
|
|
||||||
|
PATTERNS = admin_patterns("/rooms/delete_status/(?P<delete_id>[^/]+)$", "v2")
|
||||||
|
|
||||||
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
self._auth = hs.get_auth()
|
||||||
|
self._pagination_handler = hs.get_pagination_handler()
|
||||||
|
|
||||||
|
async def on_GET(
|
||||||
|
self, request: SynapseRequest, delete_id: str
|
||||||
|
) -> Tuple[int, JsonDict]:
|
||||||
|
|
||||||
|
await assert_requester_is_admin(self._auth, request)
|
||||||
|
|
||||||
|
delete_status = self._pagination_handler.get_delete_status(delete_id)
|
||||||
|
if delete_status is None:
|
||||||
|
raise NotFoundError("delete id '%s' not found" % delete_id)
|
||||||
|
|
||||||
|
return 200, cast(JsonDict, delete_status.asdict())
|
||||||
|
|
||||||
|
|
||||||
class ListRoomRestServlet(RestServlet):
|
class ListRoomRestServlet(RestServlet):
|
||||||
"""
|
"""
|
||||||
List all rooms that are known to the homeserver. Results are returned
|
List all rooms that are known to the homeserver. Results are returned
|
||||||
|
@ -474,3 +474,51 @@ class QuarantineMediaTestCase(unittest.HomeserverTestCase):
|
|||||||
% server_and_media_id_2
|
% server_and_media_id_2
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PurgeHistoryTestCase(unittest.HomeserverTestCase):
|
||||||
|
servlets = [
|
||||||
|
synapse.rest.admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare(self, reactor, clock, hs):
|
||||||
|
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")
|
||||||
|
|
||||||
|
self.room_id = self.helper.create_room_as(
|
||||||
|
self.other_user, tok=self.other_user_tok
|
||||||
|
)
|
||||||
|
self.url = f"/_synapse/admin/v1/purge_history/{self.room_id}"
|
||||||
|
self.url_status = "/_synapse/admin/v1/purge_history_status/"
|
||||||
|
|
||||||
|
def test_purge_history(self):
|
||||||
|
"""
|
||||||
|
Simple test of purge history API.
|
||||||
|
Test only that is is possible to call, get status 200 and purge_id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
self.url,
|
||||||
|
content={"delete_local_events": True, "purge_up_to_ts": 0},
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
|
self.assertIn("purge_id", channel.json_body)
|
||||||
|
purge_id = channel.json_body["purge_id"]
|
||||||
|
|
||||||
|
# get status
|
||||||
|
channel = self.make_request(
|
||||||
|
"GET",
|
||||||
|
self.url_status + purge_id,
|
||||||
|
access_token=self.admin_user_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
|
self.assertEqual("complete", channel.json_body["status"])
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user