summary refs log tree commit diff
path: root/synapse/rest
diff options
context:
space:
mode:
Diffstat (limited to 'synapse/rest')
-rw-r--r--synapse/rest/admin/__init__.py25
-rw-r--r--synapse/rest/admin/_base.py3
-rw-r--r--synapse/rest/admin/devices.py21
-rw-r--r--synapse/rest/admin/event_reports.py21
-rw-r--r--synapse/rest/admin/federation.py135
-rw-r--r--synapse/rest/admin/groups.py5
-rw-r--r--synapse/rest/admin/media.py53
-rw-r--r--synapse/rest/admin/registration_tokens.py51
-rw-r--r--synapse/rest/admin/rooms.py84
-rw-r--r--synapse/rest/admin/server_notice_servlet.py11
-rw-r--r--synapse/rest/admin/statistics.py21
-rw-r--r--synapse/rest/admin/users.py173
-rw-r--r--synapse/rest/client/login.py88
-rw-r--r--synapse/rest/client/register.py20
-rw-r--r--synapse/rest/client/relations.py16
-rw-r--r--synapse/rest/client/room.py67
-rw-r--r--synapse/rest/client/sync.py6
-rw-r--r--synapse/rest/media/v1/filepath.py115
18 files changed, 290 insertions, 625 deletions
diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py
index c499afd4be..ee4a5e481b 100644
--- a/synapse/rest/admin/__init__.py
+++ b/synapse/rest/admin/__init__.py
@@ -17,7 +17,6 @@
 
 import logging
 import platform
-from http import HTTPStatus
 from typing import TYPE_CHECKING, Optional, Tuple
 
 import synapse
@@ -40,10 +39,6 @@ from synapse.rest.admin.event_reports import (
     EventReportDetailRestServlet,
     EventReportsRestServlet,
 )
-from synapse.rest.admin.federation import (
-    DestinationsRestServlet,
-    ListDestinationsRestServlet,
-)
 from synapse.rest.admin.groups import DeleteGroupAdminRestServlet
 from synapse.rest.admin.media import ListMediaInRoom, register_servlets_for_media_repo
 from synapse.rest.admin.registration_tokens import (
@@ -103,7 +98,7 @@ class VersionServlet(RestServlet):
         }
 
     def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
-        return HTTPStatus.OK, self.res
+        return 200, self.res
 
 
 class PurgeHistoryRestServlet(RestServlet):
@@ -135,7 +130,7 @@ class PurgeHistoryRestServlet(RestServlet):
             event = await self.store.get_event(event_id)
 
             if event.room_id != room_id:
-                raise SynapseError(HTTPStatus.BAD_REQUEST, "Event is for wrong room.")
+                raise SynapseError(400, "Event is for wrong room.")
 
             # RoomStreamToken expects [int] not Optional[int]
             assert event.internal_metadata.stream_ordering is not None
@@ -149,9 +144,7 @@ class PurgeHistoryRestServlet(RestServlet):
             ts = body["purge_up_to_ts"]
             if not isinstance(ts, int):
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
-                    "purge_up_to_ts must be an int",
-                    errcode=Codes.BAD_JSON,
+                    400, "purge_up_to_ts must be an int", errcode=Codes.BAD_JSON
                 )
 
             stream_ordering = await self.store.find_first_stream_ordering_after_ts(ts)
@@ -167,9 +160,7 @@ class PurgeHistoryRestServlet(RestServlet):
                     stream_ordering,
                 )
                 raise SynapseError(
-                    HTTPStatus.NOT_FOUND,
-                    "there is no event to be purged",
-                    errcode=Codes.NOT_FOUND,
+                    404, "there is no event to be purged", errcode=Codes.NOT_FOUND
                 )
             (stream, topo, _event_id) = r
             token = "t%d-%d" % (topo, stream)
@@ -182,7 +173,7 @@ class PurgeHistoryRestServlet(RestServlet):
             )
         else:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "must specify purge_up_to_event_id or purge_up_to_ts",
                 errcode=Codes.BAD_JSON,
             )
@@ -191,7 +182,7 @@ class PurgeHistoryRestServlet(RestServlet):
             room_id, token, delete_local_events=delete_local_events
         )
 
-        return HTTPStatus.OK, {"purge_id": purge_id}
+        return 200, {"purge_id": purge_id}
 
 
 class PurgeHistoryStatusRestServlet(RestServlet):
@@ -210,7 +201,7 @@ class PurgeHistoryStatusRestServlet(RestServlet):
         if purge_status is None:
             raise NotFoundError("purge id '%s' not found" % purge_id)
 
-        return HTTPStatus.OK, purge_status.asdict()
+        return 200, purge_status.asdict()
 
 
 ########################################################################################
@@ -265,8 +256,6 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
     ListRegistrationTokensRestServlet(hs).register(http_server)
     NewRegistrationTokenRestServlet(hs).register(http_server)
     RegistrationTokenRestServlet(hs).register(http_server)
-    DestinationsRestServlet(hs).register(http_server)
-    ListDestinationsRestServlet(hs).register(http_server)
 
     # Some servlets only get registered for the main process.
     if hs.config.worker.worker_app is None:
diff --git a/synapse/rest/admin/_base.py b/synapse/rest/admin/_base.py
index 399b205aaf..d9a2f6ca15 100644
--- a/synapse/rest/admin/_base.py
+++ b/synapse/rest/admin/_base.py
@@ -13,7 +13,6 @@
 # limitations under the License.
 
 import re
-from http import HTTPStatus
 from typing import Iterable, Pattern
 
 from synapse.api.auth import Auth
@@ -63,4 +62,4 @@ async def assert_user_is_admin(auth: Auth, user_id: UserID) -> None:
     """
     is_admin = await auth.is_server_admin(user_id)
     if not is_admin:
-        raise AuthError(HTTPStatus.FORBIDDEN, "You are not a server admin")
+        raise AuthError(403, "You are not a server admin")
diff --git a/synapse/rest/admin/devices.py b/synapse/rest/admin/devices.py
index 2e5a6600d3..80fbf32f17 100644
--- a/synapse/rest/admin/devices.py
+++ b/synapse/rest/admin/devices.py
@@ -12,7 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import logging
-from http import HTTPStatus
 from typing import TYPE_CHECKING, Tuple
 
 from synapse.api.errors import NotFoundError, SynapseError
@@ -54,7 +53,7 @@ class DeviceRestServlet(RestServlet):
 
         target_user = UserID.from_string(user_id)
         if not self.hs.is_mine(target_user):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only lookup local users")
+            raise SynapseError(400, "Can only lookup local users")
 
         u = await self.store.get_user_by_id(target_user.to_string())
         if u is None:
@@ -63,7 +62,7 @@ class DeviceRestServlet(RestServlet):
         device = await self.device_handler.get_device(
             target_user.to_string(), device_id
         )
-        return HTTPStatus.OK, device
+        return 200, device
 
     async def on_DELETE(
         self, request: SynapseRequest, user_id: str, device_id: str
@@ -72,14 +71,14 @@ class DeviceRestServlet(RestServlet):
 
         target_user = UserID.from_string(user_id)
         if not self.hs.is_mine(target_user):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only lookup local users")
+            raise SynapseError(400, "Can only lookup local users")
 
         u = await self.store.get_user_by_id(target_user.to_string())
         if u is None:
             raise NotFoundError("Unknown user")
 
         await self.device_handler.delete_device(target_user.to_string(), device_id)
-        return HTTPStatus.OK, {}
+        return 200, {}
 
     async def on_PUT(
         self, request: SynapseRequest, user_id: str, device_id: str
@@ -88,7 +87,7 @@ class DeviceRestServlet(RestServlet):
 
         target_user = UserID.from_string(user_id)
         if not self.hs.is_mine(target_user):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only lookup local users")
+            raise SynapseError(400, "Can only lookup local users")
 
         u = await self.store.get_user_by_id(target_user.to_string())
         if u is None:
@@ -98,7 +97,7 @@ class DeviceRestServlet(RestServlet):
         await self.device_handler.update_device(
             target_user.to_string(), device_id, body
         )
-        return HTTPStatus.OK, {}
+        return 200, {}
 
 
 class DevicesRestServlet(RestServlet):
@@ -125,14 +124,14 @@ class DevicesRestServlet(RestServlet):
 
         target_user = UserID.from_string(user_id)
         if not self.hs.is_mine(target_user):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only lookup local users")
+            raise SynapseError(400, "Can only lookup local users")
 
         u = await self.store.get_user_by_id(target_user.to_string())
         if u is None:
             raise NotFoundError("Unknown user")
 
         devices = await self.device_handler.get_devices_by_user(target_user.to_string())
-        return HTTPStatus.OK, {"devices": devices, "total": len(devices)}
+        return 200, {"devices": devices, "total": len(devices)}
 
 
 class DeleteDevicesRestServlet(RestServlet):
@@ -156,7 +155,7 @@ class DeleteDevicesRestServlet(RestServlet):
 
         target_user = UserID.from_string(user_id)
         if not self.hs.is_mine(target_user):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only lookup local users")
+            raise SynapseError(400, "Can only lookup local users")
 
         u = await self.store.get_user_by_id(target_user.to_string())
         if u is None:
@@ -168,4 +167,4 @@ class DeleteDevicesRestServlet(RestServlet):
         await self.device_handler.delete_devices(
             target_user.to_string(), body["devices"]
         )
-        return HTTPStatus.OK, {}
+        return 200, {}
diff --git a/synapse/rest/admin/event_reports.py b/synapse/rest/admin/event_reports.py
index 5ee8b11110..bbfcaf723b 100644
--- a/synapse/rest/admin/event_reports.py
+++ b/synapse/rest/admin/event_reports.py
@@ -13,7 +13,6 @@
 # limitations under the License.
 
 import logging
-from http import HTTPStatus
 from typing import TYPE_CHECKING, Tuple
 
 from synapse.api.errors import Codes, NotFoundError, SynapseError
@@ -67,23 +66,21 @@ class EventReportsRestServlet(RestServlet):
 
         if start < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "The start parameter must be a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
 
         if limit < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "The limit parameter must be a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
 
         if direction not in ("f", "b"):
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "Unknown direction: %s" % (direction,),
-                errcode=Codes.INVALID_PARAM,
+                400, "Unknown direction: %s" % (direction,), errcode=Codes.INVALID_PARAM
             )
 
         event_reports, total = await self.store.get_event_reports_paginate(
@@ -93,7 +90,7 @@ class EventReportsRestServlet(RestServlet):
         if (start + limit) < total:
             ret["next_token"] = start + len(event_reports)
 
-        return HTTPStatus.OK, ret
+        return 200, ret
 
 
 class EventReportDetailRestServlet(RestServlet):
@@ -130,17 +127,13 @@ class EventReportDetailRestServlet(RestServlet):
         try:
             resolved_report_id = int(report_id)
         except ValueError:
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM
-            )
+            raise SynapseError(400, message, errcode=Codes.INVALID_PARAM)
 
         if resolved_report_id < 0:
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM
-            )
+            raise SynapseError(400, message, errcode=Codes.INVALID_PARAM)
 
         ret = await self.store.get_event_report(resolved_report_id)
         if not ret:
             raise NotFoundError("Event report not found")
 
-        return HTTPStatus.OK, ret
+        return 200, ret
diff --git a/synapse/rest/admin/federation.py b/synapse/rest/admin/federation.py
deleted file mode 100644
index 744687be35..0000000000
--- a/synapse/rest/admin/federation.py
+++ /dev/null
@@ -1,135 +0,0 @@
-# Copyright 2021 The Matrix.org Foundation C.I.C.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-import logging
-from http import HTTPStatus
-from typing import TYPE_CHECKING, Tuple
-
-from synapse.api.errors import Codes, NotFoundError, SynapseError
-from synapse.http.servlet import RestServlet, parse_integer, parse_string
-from synapse.http.site import SynapseRequest
-from synapse.rest.admin._base import admin_patterns, assert_requester_is_admin
-from synapse.storage.databases.main.transactions import DestinationSortOrder
-from synapse.types import JsonDict
-
-if TYPE_CHECKING:
-    from synapse.server import HomeServer
-
-logger = logging.getLogger(__name__)
-
-
-class ListDestinationsRestServlet(RestServlet):
-    """Get request to list all destinations.
-    This needs user to have administrator access in Synapse.
-
-    GET /_synapse/admin/v1/federation/destinations?from=0&limit=10
-
-    returns:
-        200 OK with list of destinations if success otherwise an error.
-
-    The parameters `from` and `limit` are required only for pagination.
-    By default, a `limit` of 100 is used.
-    The parameter `destination` can be used to filter by destination.
-    The parameter `order_by` can be used to order the result.
-    """
-
-    PATTERNS = admin_patterns("/federation/destinations$")
-
-    def __init__(self, hs: "HomeServer"):
-        self._auth = hs.get_auth()
-        self._store = hs.get_datastore()
-
-    async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
-        await assert_requester_is_admin(self._auth, request)
-
-        start = parse_integer(request, "from", default=0)
-        limit = parse_integer(request, "limit", default=100)
-
-        if start < 0:
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "Query parameter from must be a string representing a positive integer.",
-                errcode=Codes.INVALID_PARAM,
-            )
-
-        if limit < 0:
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "Query parameter limit must be a string representing a positive integer.",
-                errcode=Codes.INVALID_PARAM,
-            )
-
-        destination = parse_string(request, "destination")
-
-        order_by = parse_string(
-            request,
-            "order_by",
-            default=DestinationSortOrder.DESTINATION.value,
-            allowed_values=[dest.value for dest in DestinationSortOrder],
-        )
-
-        direction = parse_string(request, "dir", default="f", allowed_values=("f", "b"))
-
-        destinations, total = await self._store.get_destinations_paginate(
-            start, limit, destination, order_by, direction
-        )
-        response = {"destinations": destinations, "total": total}
-        if (start + limit) < total:
-            response["next_token"] = str(start + len(destinations))
-
-        return HTTPStatus.OK, response
-
-
-class DestinationsRestServlet(RestServlet):
-    """Get details of a destination.
-    This needs user to have administrator access in Synapse.
-
-    GET /_synapse/admin/v1/federation/destinations/<destination>
-
-    returns:
-        200 OK with details of a destination if success otherwise an error.
-    """
-
-    PATTERNS = admin_patterns("/federation/destinations/(?P<destination>[^/]+)$")
-
-    def __init__(self, hs: "HomeServer"):
-        self._auth = hs.get_auth()
-        self._store = hs.get_datastore()
-
-    async def on_GET(
-        self, request: SynapseRequest, destination: str
-    ) -> Tuple[int, JsonDict]:
-        await assert_requester_is_admin(self._auth, request)
-
-        destination_retry_timings = await self._store.get_destination_retry_timings(
-            destination
-        )
-
-        if not destination_retry_timings:
-            raise NotFoundError("Unknown destination")
-
-        last_successful_stream_ordering = (
-            await self._store.get_destination_last_successful_stream_ordering(
-                destination
-            )
-        )
-
-        response = {
-            "destination": destination,
-            "failure_ts": destination_retry_timings.failure_ts,
-            "retry_last_ts": destination_retry_timings.retry_last_ts,
-            "retry_interval": destination_retry_timings.retry_interval,
-            "last_successful_stream_ordering": last_successful_stream_ordering,
-        }
-
-        return HTTPStatus.OK, response
diff --git a/synapse/rest/admin/groups.py b/synapse/rest/admin/groups.py
index a27110388f..68a3ba3cb7 100644
--- a/synapse/rest/admin/groups.py
+++ b/synapse/rest/admin/groups.py
@@ -12,7 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import logging
-from http import HTTPStatus
 from typing import TYPE_CHECKING, Tuple
 
 from synapse.api.errors import SynapseError
@@ -44,7 +43,7 @@ class DeleteGroupAdminRestServlet(RestServlet):
         await assert_user_is_admin(self.auth, requester.user)
 
         if not self.is_mine_id(group_id):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only delete local groups")
+            raise SynapseError(400, "Can only delete local groups")
 
         await self.group_server.delete_group(group_id, requester.user.to_string())
-        return HTTPStatus.OK, {}
+        return 200, {}
diff --git a/synapse/rest/admin/media.py b/synapse/rest/admin/media.py
index 9e23e2d8fc..30a687d234 100644
--- a/synapse/rest/admin/media.py
+++ b/synapse/rest/admin/media.py
@@ -14,7 +14,6 @@
 # limitations under the License.
 
 import logging
-from http import HTTPStatus
 from typing import TYPE_CHECKING, Tuple
 
 from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
@@ -63,7 +62,7 @@ class QuarantineMediaInRoom(RestServlet):
             room_id, requester.user.to_string()
         )
 
-        return HTTPStatus.OK, {"num_quarantined": num_quarantined}
+        return 200, {"num_quarantined": num_quarantined}
 
 
 class QuarantineMediaByUser(RestServlet):
@@ -90,7 +89,7 @@ class QuarantineMediaByUser(RestServlet):
             user_id, requester.user.to_string()
         )
 
-        return HTTPStatus.OK, {"num_quarantined": num_quarantined}
+        return 200, {"num_quarantined": num_quarantined}
 
 
 class QuarantineMediaByID(RestServlet):
@@ -119,7 +118,7 @@ class QuarantineMediaByID(RestServlet):
             server_name, media_id, requester.user.to_string()
         )
 
-        return HTTPStatus.OK, {}
+        return 200, {}
 
 
 class UnquarantineMediaByID(RestServlet):
@@ -148,7 +147,7 @@ class UnquarantineMediaByID(RestServlet):
         # Remove from quarantine this media id
         await self.store.quarantine_media_by_id(server_name, media_id, None)
 
-        return HTTPStatus.OK, {}
+        return 200, {}
 
 
 class ProtectMediaByID(RestServlet):
@@ -171,7 +170,7 @@ class ProtectMediaByID(RestServlet):
         # Protect this media id
         await self.store.mark_local_media_as_safe(media_id, safe=True)
 
-        return HTTPStatus.OK, {}
+        return 200, {}
 
 
 class UnprotectMediaByID(RestServlet):
@@ -194,7 +193,7 @@ class UnprotectMediaByID(RestServlet):
         # Unprotect this media id
         await self.store.mark_local_media_as_safe(media_id, safe=False)
 
-        return HTTPStatus.OK, {}
+        return 200, {}
 
 
 class ListMediaInRoom(RestServlet):
@@ -212,11 +211,11 @@ class ListMediaInRoom(RestServlet):
         requester = await self.auth.get_user_by_req(request)
         is_admin = await self.auth.is_server_admin(requester.user)
         if not is_admin:
-            raise AuthError(HTTPStatus.FORBIDDEN, "You are not a server admin")
+            raise AuthError(403, "You are not a server admin")
 
         local_mxcs, remote_mxcs = await self.store.get_media_mxcs_in_room(room_id)
 
-        return HTTPStatus.OK, {"local": local_mxcs, "remote": remote_mxcs}
+        return 200, {"local": local_mxcs, "remote": remote_mxcs}
 
 
 class PurgeMediaCacheRestServlet(RestServlet):
@@ -234,13 +233,13 @@ class PurgeMediaCacheRestServlet(RestServlet):
 
         if before_ts < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter before_ts must be a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
         elif before_ts < 30000000000:  # Dec 1970 in milliseconds, Aug 2920 in seconds
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter before_ts you provided is from the year 1970. "
                 + "Double check that you are providing a timestamp in milliseconds.",
                 errcode=Codes.INVALID_PARAM,
@@ -248,7 +247,7 @@ class PurgeMediaCacheRestServlet(RestServlet):
 
         ret = await self.media_repository.delete_old_remote_media(before_ts)
 
-        return HTTPStatus.OK, ret
+        return 200, ret
 
 
 class DeleteMediaByID(RestServlet):
@@ -268,7 +267,7 @@ class DeleteMediaByID(RestServlet):
         await assert_requester_is_admin(self.auth, request)
 
         if self.server_name != server_name:
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only delete local media")
+            raise SynapseError(400, "Can only delete local media")
 
         if await self.store.get_local_media(media_id) is None:
             raise NotFoundError("Unknown media")
@@ -278,7 +277,7 @@ class DeleteMediaByID(RestServlet):
         deleted_media, total = await self.media_repository.delete_local_media_ids(
             [media_id]
         )
-        return HTTPStatus.OK, {"deleted_media": deleted_media, "total": total}
+        return 200, {"deleted_media": deleted_media, "total": total}
 
 
 class DeleteMediaByDateSize(RestServlet):
@@ -305,26 +304,26 @@ class DeleteMediaByDateSize(RestServlet):
 
         if before_ts < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter before_ts must be a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
         elif before_ts < 30000000000:  # Dec 1970 in milliseconds, Aug 2920 in seconds
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter before_ts you provided is from the year 1970. "
                 + "Double check that you are providing a timestamp in milliseconds.",
                 errcode=Codes.INVALID_PARAM,
             )
         if size_gt < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter size_gt must be a string representing a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
 
         if self.server_name != server_name:
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only delete local media")
+            raise SynapseError(400, "Can only delete local media")
 
         logging.info(
             "Deleting local media by timestamp: %s, size larger than: %s, keep profile media: %s"
@@ -334,7 +333,7 @@ class DeleteMediaByDateSize(RestServlet):
         deleted_media, total = await self.media_repository.delete_old_local_media(
             before_ts, size_gt, keep_profiles
         )
-        return HTTPStatus.OK, {"deleted_media": deleted_media, "total": total}
+        return 200, {"deleted_media": deleted_media, "total": total}
 
 
 class UserMediaRestServlet(RestServlet):
@@ -370,7 +369,7 @@ class UserMediaRestServlet(RestServlet):
         await assert_requester_is_admin(self.auth, request)
 
         if not self.is_mine(UserID.from_string(user_id)):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only look up local users")
+            raise SynapseError(400, "Can only look up local users")
 
         user = await self.store.get_user_by_id(user_id)
         if user is None:
@@ -381,14 +380,14 @@ class UserMediaRestServlet(RestServlet):
 
         if start < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter from must be a string representing a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
 
         if limit < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter limit must be a string representing a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
@@ -426,7 +425,7 @@ class UserMediaRestServlet(RestServlet):
         if (start + limit) < total:
             ret["next_token"] = start + len(media)
 
-        return HTTPStatus.OK, ret
+        return 200, ret
 
     async def on_DELETE(
         self, request: SynapseRequest, user_id: str
@@ -437,7 +436,7 @@ class UserMediaRestServlet(RestServlet):
         await assert_requester_is_admin(self.auth, request)
 
         if not self.is_mine(UserID.from_string(user_id)):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only look up local users")
+            raise SynapseError(400, "Can only look up local users")
 
         user = await self.store.get_user_by_id(user_id)
         if user is None:
@@ -448,14 +447,14 @@ class UserMediaRestServlet(RestServlet):
 
         if start < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter from must be a string representing a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
 
         if limit < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter limit must be a string representing a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
@@ -493,7 +492,7 @@ class UserMediaRestServlet(RestServlet):
             ([row["media_id"] for row in media])
         )
 
-        return HTTPStatus.OK, {"deleted_media": deleted_media, "total": total}
+        return 200, {"deleted_media": deleted_media, "total": total}
 
 
 def register_servlets_for_media_repo(hs: "HomeServer", http_server: HttpServer) -> None:
diff --git a/synapse/rest/admin/registration_tokens.py b/synapse/rest/admin/registration_tokens.py
index 891b98c088..aba48f6e7b 100644
--- a/synapse/rest/admin/registration_tokens.py
+++ b/synapse/rest/admin/registration_tokens.py
@@ -14,7 +14,6 @@
 
 import logging
 import string
-from http import HTTPStatus
 from typing import TYPE_CHECKING, Tuple
 
 from synapse.api.errors import Codes, NotFoundError, SynapseError
@@ -78,7 +77,7 @@ class ListRegistrationTokensRestServlet(RestServlet):
         await assert_requester_is_admin(self.auth, request)
         valid = parse_boolean(request, "valid")
         token_list = await self.store.get_registration_tokens(valid)
-        return HTTPStatus.OK, {"registration_tokens": token_list}
+        return 200, {"registration_tokens": token_list}
 
 
 class NewRegistrationTokenRestServlet(RestServlet):
@@ -124,20 +123,16 @@ class NewRegistrationTokenRestServlet(RestServlet):
         if "token" in body:
             token = body["token"]
             if not isinstance(token, str):
-                raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
-                    "token must be a string",
-                    Codes.INVALID_PARAM,
-                )
+                raise SynapseError(400, "token must be a string", Codes.INVALID_PARAM)
             if not (0 < len(token) <= 64):
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
+                    400,
                     "token must not be empty and must not be longer than 64 characters",
                     Codes.INVALID_PARAM,
                 )
             if not set(token).issubset(self.allowed_chars_set):
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
+                    400,
                     "token must consist only of characters matched by the regex [A-Za-z0-9-_]",
                     Codes.INVALID_PARAM,
                 )
@@ -147,13 +142,11 @@ class NewRegistrationTokenRestServlet(RestServlet):
             length = body.get("length", 16)
             if not isinstance(length, int):
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
-                    "length must be an integer",
-                    Codes.INVALID_PARAM,
+                    400, "length must be an integer", Codes.INVALID_PARAM
                 )
             if not (0 < length <= 64):
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
+                    400,
                     "length must be greater than zero and not greater than 64",
                     Codes.INVALID_PARAM,
                 )
@@ -169,7 +162,7 @@ class NewRegistrationTokenRestServlet(RestServlet):
             or (isinstance(uses_allowed, int) and uses_allowed >= 0)
         ):
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "uses_allowed must be a non-negative integer or null",
                 Codes.INVALID_PARAM,
             )
@@ -177,15 +170,11 @@ class NewRegistrationTokenRestServlet(RestServlet):
         expiry_time = body.get("expiry_time", None)
         if not isinstance(expiry_time, (int, type(None))):
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "expiry_time must be an integer or null",
-                Codes.INVALID_PARAM,
+                400, "expiry_time must be an integer or null", Codes.INVALID_PARAM
             )
         if isinstance(expiry_time, int) and expiry_time < self.clock.time_msec():
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "expiry_time must not be in the past",
-                Codes.INVALID_PARAM,
+                400, "expiry_time must not be in the past", Codes.INVALID_PARAM
             )
 
         created = await self.store.create_registration_token(
@@ -193,9 +182,7 @@ class NewRegistrationTokenRestServlet(RestServlet):
         )
         if not created:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                f"Token already exists: {token}",
-                Codes.INVALID_PARAM,
+                400, f"Token already exists: {token}", Codes.INVALID_PARAM
             )
 
         resp = {
@@ -205,7 +192,7 @@ class NewRegistrationTokenRestServlet(RestServlet):
             "completed": 0,
             "expiry_time": expiry_time,
         }
-        return HTTPStatus.OK, resp
+        return 200, resp
 
 
 class RegistrationTokenRestServlet(RestServlet):
@@ -274,7 +261,7 @@ class RegistrationTokenRestServlet(RestServlet):
         if token_info is None:
             raise NotFoundError(f"No such registration token: {token}")
 
-        return HTTPStatus.OK, token_info
+        return 200, token_info
 
     async def on_PUT(self, request: SynapseRequest, token: str) -> Tuple[int, JsonDict]:
         """Update a registration token."""
@@ -290,7 +277,7 @@ class RegistrationTokenRestServlet(RestServlet):
                 or (isinstance(uses_allowed, int) and uses_allowed >= 0)
             ):
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
+                    400,
                     "uses_allowed must be a non-negative integer or null",
                     Codes.INVALID_PARAM,
                 )
@@ -300,15 +287,11 @@ class RegistrationTokenRestServlet(RestServlet):
             expiry_time = body["expiry_time"]
             if not isinstance(expiry_time, (int, type(None))):
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
-                    "expiry_time must be an integer or null",
-                    Codes.INVALID_PARAM,
+                    400, "expiry_time must be an integer or null", Codes.INVALID_PARAM
                 )
             if isinstance(expiry_time, int) and expiry_time < self.clock.time_msec():
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
-                    "expiry_time must not be in the past",
-                    Codes.INVALID_PARAM,
+                    400, "expiry_time must not be in the past", Codes.INVALID_PARAM
                 )
             new_attributes["expiry_time"] = expiry_time
 
@@ -324,7 +307,7 @@ class RegistrationTokenRestServlet(RestServlet):
         if token_info is None:
             raise NotFoundError(f"No such registration token: {token}")
 
-        return HTTPStatus.OK, token_info
+        return 200, token_info
 
     async def on_DELETE(
         self, request: SynapseRequest, token: str
@@ -333,6 +316,6 @@ class RegistrationTokenRestServlet(RestServlet):
         await assert_requester_is_admin(self.auth, request)
 
         if await self.store.delete_registration_token(token):
-            return HTTPStatus.OK, {}
+            return 200, {}
 
         raise NotFoundError(f"No such registration token: {token}")
diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py
index 829e86675a..a89dda1ba5 100644
--- a/synapse/rest/admin/rooms.py
+++ b/synapse/rest/admin/rooms.py
@@ -102,9 +102,10 @@ class RoomRestV2Servlet(RestServlet):
             )
 
         if not RoomID.is_valid(room_id):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "%s is not a legal room ID" % (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,
@@ -117,7 +118,7 @@ class RoomRestV2Servlet(RestServlet):
             force_purge=force_purge,
         )
 
-        return HTTPStatus.OK, {"delete_id": delete_id}
+        return 200, {"delete_id": delete_id}
 
 
 class DeleteRoomStatusByRoomIdRestServlet(RestServlet):
@@ -136,9 +137,7 @@ class DeleteRoomStatusByRoomIdRestServlet(RestServlet):
         await assert_requester_is_admin(self._auth, request)
 
         if not RoomID.is_valid(room_id):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "%s is not a legal room ID" % (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:
@@ -154,7 +153,7 @@ class DeleteRoomStatusByRoomIdRestServlet(RestServlet):
                         **delete.asdict(),
                     }
                 ]
-        return HTTPStatus.OK, {"results": cast(JsonDict, response)}
+        return 200, {"results": cast(JsonDict, response)}
 
 
 class DeleteRoomStatusByDeleteIdRestServlet(RestServlet):
@@ -176,7 +175,7 @@ class DeleteRoomStatusByDeleteIdRestServlet(RestServlet):
         if delete_status is None:
             raise NotFoundError("delete id '%s' not found" % delete_id)
 
-        return HTTPStatus.OK, cast(JsonDict, delete_status.asdict())
+        return 200, cast(JsonDict, delete_status.asdict())
 
 
 class ListRoomRestServlet(RestServlet):
@@ -218,7 +217,7 @@ class ListRoomRestServlet(RestServlet):
             RoomSortOrder.STATE_EVENTS.value,
         ):
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Unknown value for order_by: %s" % (order_by,),
                 errcode=Codes.INVALID_PARAM,
             )
@@ -226,7 +225,7 @@ class ListRoomRestServlet(RestServlet):
         search_term = parse_string(request, "search_term", encoding="utf-8")
         if search_term == "":
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "search_term cannot be an empty string",
                 errcode=Codes.INVALID_PARAM,
             )
@@ -234,9 +233,7 @@ class ListRoomRestServlet(RestServlet):
         direction = parse_string(request, "dir", default="f")
         if direction not in ("f", "b"):
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "Unknown direction: %s" % (direction,),
-                errcode=Codes.INVALID_PARAM,
+                400, "Unknown direction: %s" % (direction,), errcode=Codes.INVALID_PARAM
             )
 
         reverse_order = True if direction == "b" else False
@@ -268,7 +265,7 @@ class ListRoomRestServlet(RestServlet):
             else:
                 response["prev_batch"] = 0
 
-        return HTTPStatus.OK, response
+        return 200, response
 
 
 class RoomRestServlet(RestServlet):
@@ -313,7 +310,7 @@ class RoomRestServlet(RestServlet):
         members = await self.store.get_users_in_room(room_id)
         ret["joined_local_devices"] = await self.store.count_devices_by_users(members)
 
-        return HTTPStatus.OK, ret
+        return 200, ret
 
     async def on_DELETE(
         self, request: SynapseRequest, room_id: str
@@ -389,7 +386,7 @@ class RoomRestServlet(RestServlet):
         # See https://github.com/python/mypy/issues/4976#issuecomment-579883622
         # for some discussion on why this is necessary. Either way,
         # `ret` is an opaque dictionary blob as far as the rest of the app cares.
-        return HTTPStatus.OK, cast(JsonDict, ret)
+        return 200, cast(JsonDict, ret)
 
 
 class RoomMembersRestServlet(RestServlet):
@@ -416,7 +413,7 @@ class RoomMembersRestServlet(RestServlet):
         members = await self.store.get_users_in_room(room_id)
         ret = {"members": members, "total": len(members)}
 
-        return HTTPStatus.OK, ret
+        return 200, ret
 
 
 class RoomStateRestServlet(RestServlet):
@@ -446,10 +443,16 @@ class RoomStateRestServlet(RestServlet):
         event_ids = await self.store.get_current_state_ids(room_id)
         events = await self.store.get_events(event_ids.values())
         now = self.clock.time_msec()
-        room_state = await self._event_serializer.serialize_events(events.values(), now)
+        room_state = await self._event_serializer.serialize_events(
+            events.values(),
+            now,
+            # We don't bother bundling aggregations in when asked for state
+            # events, as clients won't use them.
+            bundle_relations=False,
+        )
         ret = {"state": room_state}
 
-        return HTTPStatus.OK, ret
+        return 200, ret
 
 
 class JoinRoomAliasServlet(ResolveRoomIdMixin, RestServlet):
@@ -478,10 +481,7 @@ class JoinRoomAliasServlet(ResolveRoomIdMixin, RestServlet):
         target_user = UserID.from_string(content["user_id"])
 
         if not self.hs.is_mine(target_user):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "This endpoint can only be used with local users",
-            )
+            raise SynapseError(400, "This endpoint can only be used with local users")
 
         if not await self.admin_handler.get_user(target_user):
             raise NotFoundError("User not found")
@@ -527,7 +527,7 @@ class JoinRoomAliasServlet(ResolveRoomIdMixin, RestServlet):
             ratelimit=False,
         )
 
-        return HTTPStatus.OK, {"room_id": room_id}
+        return 200, {"room_id": room_id}
 
 
 class MakeRoomAdminRestServlet(ResolveRoomIdMixin, RestServlet):
@@ -568,7 +568,7 @@ class MakeRoomAdminRestServlet(ResolveRoomIdMixin, RestServlet):
         # Figure out which local users currently have power in the room, if any.
         room_state = await self.state_handler.get_current_state(room_id)
         if not room_state:
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Server not in room")
+            raise SynapseError(400, "Server not in room")
 
         create_event = room_state[(EventTypes.Create, "")]
         power_levels = room_state.get((EventTypes.PowerLevels, ""))
@@ -582,9 +582,7 @@ class MakeRoomAdminRestServlet(ResolveRoomIdMixin, RestServlet):
             admin_users.sort(key=lambda user: user_power[user])
 
             if not admin_users:
-                raise SynapseError(
-                    HTTPStatus.BAD_REQUEST, "No local admin user in room"
-                )
+                raise SynapseError(400, "No local admin user in room")
 
             admin_user_id = None
 
@@ -601,7 +599,7 @@ class MakeRoomAdminRestServlet(ResolveRoomIdMixin, RestServlet):
 
             if not admin_user_id:
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
+                    400,
                     "No local admin user in room",
                 )
 
@@ -612,7 +610,7 @@ class MakeRoomAdminRestServlet(ResolveRoomIdMixin, RestServlet):
             admin_user_id = create_event.sender
             if not self.is_mine_id(admin_user_id):
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
+                    400,
                     "No local admin user in room",
                 )
 
@@ -641,8 +639,7 @@ class MakeRoomAdminRestServlet(ResolveRoomIdMixin, RestServlet):
         except AuthError:
             # The admin user we found turned out not to have enough power.
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "No local admin user in room with power to update power levels.",
+                400, "No local admin user in room with power to update power levels."
             )
 
         # Now we check if the user we're granting admin rights to is already in
@@ -656,7 +653,7 @@ class MakeRoomAdminRestServlet(ResolveRoomIdMixin, RestServlet):
             )
 
         if is_joined:
-            return HTTPStatus.OK, {}
+            return 200, {}
 
         join_rules = room_state.get((EventTypes.JoinRules, ""))
         is_public = False
@@ -664,7 +661,7 @@ class MakeRoomAdminRestServlet(ResolveRoomIdMixin, RestServlet):
             is_public = join_rules.content.get("join_rule") == JoinRules.PUBLIC
 
         if is_public:
-            return HTTPStatus.OK, {}
+            return 200, {}
 
         await self.room_member_handler.update_membership(
             fake_requester,
@@ -673,7 +670,7 @@ class MakeRoomAdminRestServlet(ResolveRoomIdMixin, RestServlet):
             action=Membership.INVITE,
         )
 
-        return HTTPStatus.OK, {}
+        return 200, {}
 
 
 class ForwardExtremitiesRestServlet(ResolveRoomIdMixin, RestServlet):
@@ -705,7 +702,7 @@ class ForwardExtremitiesRestServlet(ResolveRoomIdMixin, RestServlet):
         room_id, _ = await self.resolve_room_id(room_identifier)
 
         deleted_count = await self.store.delete_forward_extremities_for_room(room_id)
-        return HTTPStatus.OK, {"deleted": deleted_count}
+        return 200, {"deleted": deleted_count}
 
     async def on_GET(
         self, request: SynapseRequest, room_identifier: str
@@ -716,7 +713,7 @@ class ForwardExtremitiesRestServlet(ResolveRoomIdMixin, RestServlet):
         room_id, _ = await self.resolve_room_id(room_identifier)
 
         extremities = await self.store.get_forward_extremities_for_room(room_id)
-        return HTTPStatus.OK, {"count": len(extremities), "results": extremities}
+        return 200, {"count": len(extremities), "results": extremities}
 
 
 class RoomEventContextServlet(RestServlet):
@@ -765,9 +762,7 @@ class RoomEventContextServlet(RestServlet):
         )
 
         if not results:
-            raise SynapseError(
-                HTTPStatus.NOT_FOUND, "Event not found.", errcode=Codes.NOT_FOUND
-            )
+            raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND)
 
         time_now = self.clock.time_msec()
         results["events_before"] = await self._event_serializer.serialize_events(
@@ -780,10 +775,13 @@ class RoomEventContextServlet(RestServlet):
             results["events_after"], time_now
         )
         results["state"] = await self._event_serializer.serialize_events(
-            results["state"], time_now
+            results["state"],
+            time_now,
+            # No need to bundle aggregations for state events
+            bundle_relations=False,
         )
 
-        return HTTPStatus.OK, results
+        return 200, results
 
 
 class BlockRoomRestServlet(RestServlet):
diff --git a/synapse/rest/admin/server_notice_servlet.py b/synapse/rest/admin/server_notice_servlet.py
index b295fb078b..19f84f33f2 100644
--- a/synapse/rest/admin/server_notice_servlet.py
+++ b/synapse/rest/admin/server_notice_servlet.py
@@ -11,7 +11,6 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-from http import HTTPStatus
 from typing import TYPE_CHECKING, Awaitable, Optional, Tuple
 
 from synapse.api.constants import EventTypes
@@ -83,15 +82,11 @@ class SendServerNoticeServlet(RestServlet):
         # but worker processes still need to initialise SendServerNoticeServlet (as it is part of the
         # admin api).
         if not self.server_notices_manager.is_enabled():
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "Server notices are not enabled on this server"
-            )
+            raise SynapseError(400, "Server notices are not enabled on this server")
 
         target_user = UserID.from_string(body["user_id"])
         if not self.hs.is_mine(target_user):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "Server notices can only be sent to local users"
-            )
+            raise SynapseError(400, "Server notices can only be sent to local users")
 
         if not await self.admin_handler.get_user(target_user):
             raise NotFoundError("User not found")
@@ -104,7 +99,7 @@ class SendServerNoticeServlet(RestServlet):
             txn_id=txn_id,
         )
 
-        return HTTPStatus.OK, {"event_id": event.event_id}
+        return 200, {"event_id": event.event_id}
 
     def on_PUT(
         self, request: SynapseRequest, txn_id: str
diff --git a/synapse/rest/admin/statistics.py b/synapse/rest/admin/statistics.py
index ca41fd45f2..948de94ccd 100644
--- a/synapse/rest/admin/statistics.py
+++ b/synapse/rest/admin/statistics.py
@@ -13,7 +13,6 @@
 # limitations under the License.
 
 import logging
-from http import HTTPStatus
 from typing import TYPE_CHECKING, Tuple
 
 from synapse.api.errors import Codes, SynapseError
@@ -54,7 +53,7 @@ class UserMediaStatisticsRestServlet(RestServlet):
             UserSortOrder.DISPLAYNAME.value,
         ):
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Unknown value for order_by: %s" % (order_by,),
                 errcode=Codes.INVALID_PARAM,
             )
@@ -62,7 +61,7 @@ class UserMediaStatisticsRestServlet(RestServlet):
         start = parse_integer(request, "from", default=0)
         if start < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter from must be a string representing a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
@@ -70,7 +69,7 @@ class UserMediaStatisticsRestServlet(RestServlet):
         limit = parse_integer(request, "limit", default=100)
         if limit < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter limit must be a string representing a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
@@ -78,7 +77,7 @@ class UserMediaStatisticsRestServlet(RestServlet):
         from_ts = parse_integer(request, "from_ts", default=0)
         if from_ts < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter from_ts must be a string representing a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
@@ -87,13 +86,13 @@ class UserMediaStatisticsRestServlet(RestServlet):
         if until_ts is not None:
             if until_ts < 0:
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
+                    400,
                     "Query parameter until_ts must be a string representing a positive integer.",
                     errcode=Codes.INVALID_PARAM,
                 )
             if until_ts <= from_ts:
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
+                    400,
                     "Query parameter until_ts must be greater than from_ts.",
                     errcode=Codes.INVALID_PARAM,
                 )
@@ -101,7 +100,7 @@ class UserMediaStatisticsRestServlet(RestServlet):
         search_term = parse_string(request, "search_term")
         if search_term == "":
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter search_term cannot be an empty string.",
                 errcode=Codes.INVALID_PARAM,
             )
@@ -109,9 +108,7 @@ class UserMediaStatisticsRestServlet(RestServlet):
         direction = parse_string(request, "dir", default="f")
         if direction not in ("f", "b"):
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "Unknown direction: %s" % (direction,),
-                errcode=Codes.INVALID_PARAM,
+                400, "Unknown direction: %s" % (direction,), errcode=Codes.INVALID_PARAM
             )
 
         users_media, total = await self.store.get_users_media_usage_paginate(
@@ -121,4 +118,4 @@ class UserMediaStatisticsRestServlet(RestServlet):
         if (start + limit) < total:
             ret["next_token"] = start + len(users_media)
 
-        return HTTPStatus.OK, ret
+        return 200, ret
diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py
index 2a60b602b1..ccd9a2a175 100644
--- a/synapse/rest/admin/users.py
+++ b/synapse/rest/admin/users.py
@@ -79,14 +79,14 @@ class UsersRestServletV2(RestServlet):
 
         if start < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter from must be a string representing a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
 
         if limit < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "Query parameter limit must be a string representing a positive integer.",
                 errcode=Codes.INVALID_PARAM,
             )
@@ -122,7 +122,7 @@ class UsersRestServletV2(RestServlet):
         if (start + limit) < total:
             ret["next_token"] = str(start + len(users))
 
-        return HTTPStatus.OK, ret
+        return 200, ret
 
 
 class UserRestServletV2(RestServlet):
@@ -172,14 +172,14 @@ class UserRestServletV2(RestServlet):
 
         target_user = UserID.from_string(user_id)
         if not self.hs.is_mine(target_user):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only look up local users")
+            raise SynapseError(400, "Can only look up local users")
 
         ret = await self.admin_handler.get_user(target_user)
 
         if not ret:
             raise NotFoundError("User not found")
 
-        return HTTPStatus.OK, ret
+        return 200, ret
 
     async def on_PUT(
         self, request: SynapseRequest, user_id: str
@@ -191,10 +191,7 @@ class UserRestServletV2(RestServlet):
         body = parse_json_object_from_request(request)
 
         if not self.hs.is_mine(target_user):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "This endpoint can only be used with local users",
-            )
+            raise SynapseError(400, "This endpoint can only be used with local users")
 
         user = await self.admin_handler.get_user(target_user)
         user_id = target_user.to_string()
@@ -213,7 +210,7 @@ class UserRestServletV2(RestServlet):
 
         user_type = body.get("user_type", None)
         if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES:
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid user type")
+            raise SynapseError(400, "Invalid user type")
 
         set_admin_to = body.get("admin", False)
         if not isinstance(set_admin_to, bool):
@@ -226,13 +223,11 @@ class UserRestServletV2(RestServlet):
         password = body.get("password", None)
         if password is not None:
             if not isinstance(password, str) or len(password) > 512:
-                raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid password")
+                raise SynapseError(400, "Invalid password")
 
         deactivate = body.get("deactivated", False)
         if not isinstance(deactivate, bool):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "'deactivated' parameter is not of type boolean"
-            )
+            raise SynapseError(400, "'deactivated' parameter is not of type boolean")
 
         # convert List[Dict[str, str]] into List[Tuple[str, str]]
         if external_ids is not None:
@@ -287,9 +282,7 @@ class UserRestServletV2(RestServlet):
                         user_id,
                     )
                 except ExternalIDReuseException:
-                    raise SynapseError(
-                        HTTPStatus.CONFLICT, "External id is already in use."
-                    )
+                    raise SynapseError(409, "External id is already in use.")
 
             if "avatar_url" in body and isinstance(body["avatar_url"], str):
                 await self.profile_handler.set_avatar_url(
@@ -300,9 +293,7 @@ class UserRestServletV2(RestServlet):
                 if set_admin_to != user["admin"]:
                     auth_user = requester.user
                     if target_user == auth_user and not set_admin_to:
-                        raise SynapseError(
-                            HTTPStatus.BAD_REQUEST, "You may not demote yourself."
-                        )
+                        raise SynapseError(400, "You may not demote yourself.")
 
                     await self.store.set_server_admin(target_user, set_admin_to)
 
@@ -328,8 +319,7 @@ class UserRestServletV2(RestServlet):
                         and self.auth_handler.can_change_password()
                     ):
                         raise SynapseError(
-                            HTTPStatus.BAD_REQUEST,
-                            "Must provide a password to re-activate an account.",
+                            400, "Must provide a password to re-activate an account."
                         )
 
                     await self.deactivate_account_handler.activate_account(
@@ -342,7 +332,7 @@ class UserRestServletV2(RestServlet):
             user = await self.admin_handler.get_user(target_user)
             assert user is not None
 
-            return HTTPStatus.OK, user
+            return 200, user
 
         else:  # create user
             displayname = body.get("displayname", None)
@@ -391,9 +381,7 @@ class UserRestServletV2(RestServlet):
                             user_id,
                         )
                 except ExternalIDReuseException:
-                    raise SynapseError(
-                        HTTPStatus.CONFLICT, "External id is already in use."
-                    )
+                    raise SynapseError(409, "External id is already in use.")
 
             if "avatar_url" in body and isinstance(body["avatar_url"], str):
                 await self.profile_handler.set_avatar_url(
@@ -441,61 +429,51 @@ class UserRegisterServlet(RestServlet):
 
         nonce = secrets.token_hex(64)
         self.nonces[nonce] = int(self.reactor.seconds())
-        return HTTPStatus.OK, {"nonce": nonce}
+        return 200, {"nonce": nonce}
 
     async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
         self._clear_old_nonces()
 
         if not self.hs.config.registration.registration_shared_secret:
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "Shared secret registration is not enabled"
-            )
+            raise SynapseError(400, "Shared secret registration is not enabled")
 
         body = parse_json_object_from_request(request)
 
         if "nonce" not in body:
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "nonce must be specified",
-                errcode=Codes.BAD_JSON,
-            )
+            raise SynapseError(400, "nonce must be specified", errcode=Codes.BAD_JSON)
 
         nonce = body["nonce"]
 
         if nonce not in self.nonces:
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "unrecognised nonce")
+            raise SynapseError(400, "unrecognised nonce")
 
         # Delete the nonce, so it can't be reused, even if it's invalid
         del self.nonces[nonce]
 
         if "username" not in body:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "username must be specified",
-                errcode=Codes.BAD_JSON,
+                400, "username must be specified", errcode=Codes.BAD_JSON
             )
         else:
             if not isinstance(body["username"], str) or len(body["username"]) > 512:
-                raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid username")
+                raise SynapseError(400, "Invalid username")
 
             username = body["username"].encode("utf-8")
             if b"\x00" in username:
-                raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid username")
+                raise SynapseError(400, "Invalid username")
 
         if "password" not in body:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "password must be specified",
-                errcode=Codes.BAD_JSON,
+                400, "password must be specified", errcode=Codes.BAD_JSON
             )
         else:
             password = body["password"]
             if not isinstance(password, str) or len(password) > 512:
-                raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid password")
+                raise SynapseError(400, "Invalid password")
 
             password_bytes = password.encode("utf-8")
             if b"\x00" in password_bytes:
-                raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid password")
+                raise SynapseError(400, "Invalid password")
 
             password_hash = await self.auth_handler.hash(password)
 
@@ -504,12 +482,10 @@ class UserRegisterServlet(RestServlet):
         displayname = body.get("displayname", None)
 
         if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES:
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid user type")
+            raise SynapseError(400, "Invalid user type")
 
         if "mac" not in body:
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "mac must be specified", errcode=Codes.BAD_JSON
-            )
+            raise SynapseError(400, "mac must be specified", errcode=Codes.BAD_JSON)
 
         got_mac = body["mac"]
 
@@ -531,7 +507,7 @@ class UserRegisterServlet(RestServlet):
         want_mac = want_mac_builder.hexdigest()
 
         if not hmac.compare_digest(want_mac.encode("ascii"), got_mac.encode("ascii")):
-            raise SynapseError(HTTPStatus.FORBIDDEN, "HMAC incorrect")
+            raise SynapseError(403, "HMAC incorrect")
 
         # Reuse the parts of RegisterRestServlet to reduce code duplication
         from synapse.rest.client.register import RegisterRestServlet
@@ -548,7 +524,7 @@ class UserRegisterServlet(RestServlet):
         )
 
         result = await register._create_registration_details(user_id, body)
-        return HTTPStatus.OK, result
+        return 200, result
 
 
 class WhoisRestServlet(RestServlet):
@@ -576,11 +552,11 @@ class WhoisRestServlet(RestServlet):
             await assert_user_is_admin(self.auth, auth_user)
 
         if not self.hs.is_mine(target_user):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only whois a local user")
+            raise SynapseError(400, "Can only whois a local user")
 
         ret = await self.admin_handler.get_whois(target_user)
 
-        return HTTPStatus.OK, ret
+        return 200, ret
 
 
 class DeactivateAccountRestServlet(RestServlet):
@@ -599,9 +575,7 @@ class DeactivateAccountRestServlet(RestServlet):
         await assert_user_is_admin(self.auth, requester.user)
 
         if not self.is_mine(UserID.from_string(target_user_id)):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "Can only deactivate local users"
-            )
+            raise SynapseError(400, "Can only deactivate local users")
 
         if not await self.store.get_user_by_id(target_user_id):
             raise NotFoundError("User not found")
@@ -623,7 +597,7 @@ class DeactivateAccountRestServlet(RestServlet):
         else:
             id_server_unbind_result = "no-support"
 
-        return HTTPStatus.OK, {"id_server_unbind_result": id_server_unbind_result}
+        return 200, {"id_server_unbind_result": id_server_unbind_result}
 
 
 class AccountValidityRenewServlet(RestServlet):
@@ -646,7 +620,7 @@ class AccountValidityRenewServlet(RestServlet):
 
             if "user_id" not in body:
                 raise SynapseError(
-                    HTTPStatus.BAD_REQUEST,
+                    400,
                     "Missing property 'user_id' in the request body",
                 )
 
@@ -657,7 +631,7 @@ class AccountValidityRenewServlet(RestServlet):
             )
 
         res = {"expiration_ts": expiration_ts}
-        return HTTPStatus.OK, res
+        return 200, res
 
 
 class ResetPasswordRestServlet(RestServlet):
@@ -704,7 +678,7 @@ class ResetPasswordRestServlet(RestServlet):
         await self._set_password_handler.set_password(
             target_user_id, new_password_hash, logout_devices, requester
         )
-        return HTTPStatus.OK, {}
+        return 200, {}
 
 
 class SearchUsersRestServlet(RestServlet):
@@ -738,16 +712,16 @@ class SearchUsersRestServlet(RestServlet):
 
         # To allow all users to get the users list
         # if not is_admin and target_user != auth_user:
-        #     raise AuthError(HTTPStatus.FORBIDDEN, "You are not a server admin")
+        #     raise AuthError(403, "You are not a server admin")
 
         if not self.hs.is_mine(target_user):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only users a local user")
+            raise SynapseError(400, "Can only users a local user")
 
         term = parse_string(request, "term", required=True)
         logger.info("term: %s ", term)
 
         ret = await self.store.search_users(term)
-        return HTTPStatus.OK, ret
+        return 200, ret
 
 
 class UserAdminServlet(RestServlet):
@@ -791,14 +765,11 @@ class UserAdminServlet(RestServlet):
         target_user = UserID.from_string(user_id)
 
         if not self.hs.is_mine(target_user):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "Only local users can be admins of this homeserver",
-            )
+            raise SynapseError(400, "Only local users can be admins of this homeserver")
 
         is_admin = await self.store.is_server_admin(target_user)
 
-        return HTTPStatus.OK, {"admin": is_admin}
+        return 200, {"admin": is_admin}
 
     async def on_PUT(
         self, request: SynapseRequest, user_id: str
@@ -814,19 +785,16 @@ class UserAdminServlet(RestServlet):
         assert_params_in_dict(body, ["admin"])
 
         if not self.hs.is_mine(target_user):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
-                "Only local users can be admins of this homeserver",
-            )
+            raise SynapseError(400, "Only local users can be admins of this homeserver")
 
         set_admin_to = bool(body["admin"])
 
         if target_user == auth_user and not set_admin_to:
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "You may not demote yourself.")
+            raise SynapseError(400, "You may not demote yourself.")
 
         await self.store.set_server_admin(target_user, set_admin_to)
 
-        return HTTPStatus.OK, {}
+        return 200, {}
 
 
 class UserMembershipRestServlet(RestServlet):
@@ -848,7 +816,7 @@ class UserMembershipRestServlet(RestServlet):
 
         room_ids = await self.store.get_rooms_for_user(user_id)
         ret = {"joined_rooms": list(room_ids), "total": len(room_ids)}
-        return HTTPStatus.OK, ret
+        return 200, ret
 
 
 class PushersRestServlet(RestServlet):
@@ -877,7 +845,7 @@ class PushersRestServlet(RestServlet):
         await assert_requester_is_admin(self.auth, request)
 
         if not self.is_mine(UserID.from_string(user_id)):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only look up local users")
+            raise SynapseError(400, "Can only look up local users")
 
         if not await self.store.get_user_by_id(user_id):
             raise NotFoundError("User not found")
@@ -886,10 +854,7 @@ class PushersRestServlet(RestServlet):
 
         filtered_pushers = [p.as_dict() for p in pushers]
 
-        return HTTPStatus.OK, {
-            "pushers": filtered_pushers,
-            "total": len(filtered_pushers),
-        }
+        return 200, {"pushers": filtered_pushers, "total": len(filtered_pushers)}
 
 
 class UserTokenRestServlet(RestServlet):
@@ -922,22 +887,16 @@ class UserTokenRestServlet(RestServlet):
         auth_user = requester.user
 
         if not self.hs.is_mine_id(user_id):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "Only local users can be logged in as"
-            )
+            raise SynapseError(400, "Only local users can be logged in as")
 
         body = parse_json_object_from_request(request, allow_empty_body=True)
 
         valid_until_ms = body.get("valid_until_ms")
         if valid_until_ms and not isinstance(valid_until_ms, int):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "'valid_until_ms' parameter must be an int"
-            )
+            raise SynapseError(400, "'valid_until_ms' parameter must be an int")
 
         if auth_user.to_string() == user_id:
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "Cannot use admin API to login as self"
-            )
+            raise SynapseError(400, "Cannot use admin API to login as self")
 
         token = await self.auth_handler.create_access_token_for_user_id(
             user_id=auth_user.to_string(),
@@ -946,7 +905,7 @@ class UserTokenRestServlet(RestServlet):
             puppets_user_id=user_id,
         )
 
-        return HTTPStatus.OK, {"access_token": token}
+        return 200, {"access_token": token}
 
 
 class ShadowBanRestServlet(RestServlet):
@@ -988,13 +947,11 @@ class ShadowBanRestServlet(RestServlet):
         await assert_requester_is_admin(self.auth, request)
 
         if not self.hs.is_mine_id(user_id):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "Only local users can be shadow-banned"
-            )
+            raise SynapseError(400, "Only local users can be shadow-banned")
 
         await self.store.set_shadow_banned(UserID.from_string(user_id), True)
 
-        return HTTPStatus.OK, {}
+        return 200, {}
 
     async def on_DELETE(
         self, request: SynapseRequest, user_id: str
@@ -1002,13 +959,11 @@ class ShadowBanRestServlet(RestServlet):
         await assert_requester_is_admin(self.auth, request)
 
         if not self.hs.is_mine_id(user_id):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "Only local users can be shadow-banned"
-            )
+            raise SynapseError(400, "Only local users can be shadow-banned")
 
         await self.store.set_shadow_banned(UserID.from_string(user_id), False)
 
-        return HTTPStatus.OK, {}
+        return 200, {}
 
 
 class RateLimitRestServlet(RestServlet):
@@ -1040,7 +995,7 @@ class RateLimitRestServlet(RestServlet):
         await assert_requester_is_admin(self.auth, request)
 
         if not self.hs.is_mine_id(user_id):
-            raise SynapseError(HTTPStatus.BAD_REQUEST, "Can only look up local users")
+            raise SynapseError(400, "Can only look up local users")
 
         if not await self.store.get_user_by_id(user_id):
             raise NotFoundError("User not found")
@@ -1061,7 +1016,7 @@ class RateLimitRestServlet(RestServlet):
         else:
             ret = {}
 
-        return HTTPStatus.OK, ret
+        return 200, ret
 
     async def on_POST(
         self, request: SynapseRequest, user_id: str
@@ -1069,9 +1024,7 @@ class RateLimitRestServlet(RestServlet):
         await assert_requester_is_admin(self.auth, request)
 
         if not self.hs.is_mine_id(user_id):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "Only local users can be ratelimited"
-            )
+            raise SynapseError(400, "Only local users can be ratelimited")
 
         if not await self.store.get_user_by_id(user_id):
             raise NotFoundError("User not found")
@@ -1083,14 +1036,14 @@ class RateLimitRestServlet(RestServlet):
 
         if not isinstance(messages_per_second, int) or messages_per_second < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "%r parameter must be a positive int" % (messages_per_second,),
                 errcode=Codes.INVALID_PARAM,
             )
 
         if not isinstance(burst_count, int) or burst_count < 0:
             raise SynapseError(
-                HTTPStatus.BAD_REQUEST,
+                400,
                 "%r parameter must be a positive int" % (burst_count,),
                 errcode=Codes.INVALID_PARAM,
             )
@@ -1106,7 +1059,7 @@ class RateLimitRestServlet(RestServlet):
             "burst_count": ratelimit.burst_count,
         }
 
-        return HTTPStatus.OK, ret
+        return 200, ret
 
     async def on_DELETE(
         self, request: SynapseRequest, user_id: str
@@ -1114,13 +1067,11 @@ class RateLimitRestServlet(RestServlet):
         await assert_requester_is_admin(self.auth, request)
 
         if not self.hs.is_mine_id(user_id):
-            raise SynapseError(
-                HTTPStatus.BAD_REQUEST, "Only local users can be ratelimited"
-            )
+            raise SynapseError(400, "Only local users can be ratelimited")
 
         if not await self.store.get_user_by_id(user_id):
             raise NotFoundError("User not found")
 
         await self.store.delete_ratelimit_for_user(user_id)
 
-        return HTTPStatus.OK, {}
+        return 200, {}
diff --git a/synapse/rest/client/login.py b/synapse/rest/client/login.py
index f9994658c4..67e03dca04 100644
--- a/synapse/rest/client/login.py
+++ b/synapse/rest/client/login.py
@@ -14,17 +14,7 @@
 
 import logging
 import re
-from typing import (
-    TYPE_CHECKING,
-    Any,
-    Awaitable,
-    Callable,
-    Dict,
-    List,
-    Optional,
-    Tuple,
-    Union,
-)
+from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Tuple
 
 from typing_extensions import TypedDict
 
@@ -38,6 +28,7 @@ from synapse.http.server import HttpServer, finish_request
 from synapse.http.servlet import (
     RestServlet,
     assert_params_in_dict,
+    parse_boolean,
     parse_bytes_from_args,
     parse_json_object_from_request,
     parse_string,
@@ -72,7 +63,7 @@ class LoginRestServlet(RestServlet):
     JWT_TYPE_DEPRECATED = "m.login.jwt"
     APPSERVICE_TYPE = "m.login.application_service"
     APPSERVICE_TYPE_UNSTABLE = "uk.half-shot.msc2778.login.application_service"
-    REFRESH_TOKEN_PARAM = "refresh_token"
+    REFRESH_TOKEN_PARAM = "org.matrix.msc2918.refresh_token"
 
     def __init__(self, hs: "HomeServer"):
         super().__init__()
@@ -90,7 +81,7 @@ class LoginRestServlet(RestServlet):
         self.saml2_enabled = hs.config.saml2.saml2_enabled
         self.cas_enabled = hs.config.cas.cas_enabled
         self.oidc_enabled = hs.config.oidc.oidc_enabled
-        self._refresh_tokens_enabled = (
+        self._msc2918_enabled = (
             hs.config.registration.refreshable_access_token_lifetime is not None
         )
 
@@ -163,16 +154,14 @@ class LoginRestServlet(RestServlet):
     async def on_POST(self, request: SynapseRequest) -> Tuple[int, LoginResponse]:
         login_submission = parse_json_object_from_request(request)
 
-        # Check to see if the client requested a refresh token.
-        client_requested_refresh_token = login_submission.get(
-            LoginRestServlet.REFRESH_TOKEN_PARAM, False
-        )
-        if not isinstance(client_requested_refresh_token, bool):
-            raise SynapseError(400, "`refresh_token` should be true or false.")
-
-        should_issue_refresh_token = (
-            self._refresh_tokens_enabled and client_requested_refresh_token
-        )
+        if self._msc2918_enabled:
+            # Check if this login should also issue a refresh token, as per
+            # MSC2918
+            should_issue_refresh_token = parse_boolean(
+                request, name=LoginRestServlet.REFRESH_TOKEN_PARAM, default=False
+            )
+        else:
+            should_issue_refresh_token = False
 
         try:
             if login_submission["type"] in (
@@ -302,7 +291,6 @@ class LoginRestServlet(RestServlet):
         ratelimit: bool = True,
         auth_provider_id: Optional[str] = None,
         should_issue_refresh_token: bool = False,
-        auth_provider_session_id: Optional[str] = None,
     ) -> LoginResponse:
         """Called when we've successfully authed the user and now need to
         actually login them in (e.g. create devices). This gets called on
@@ -318,10 +306,10 @@ class LoginRestServlet(RestServlet):
             create_non_existent_users: Whether to create the user if they don't
                 exist. Defaults to False.
             ratelimit: Whether to ratelimit the login request.
-            auth_provider_id: The SSO IdP the user used, if any.
+            auth_provider_id: The SSO IdP the user used, if any (just used for the
+                prometheus metrics).
             should_issue_refresh_token: True if this login should issue
                 a refresh token alongside the access token.
-            auth_provider_session_id: The session ID got during login from the SSO IdP.
 
         Returns:
             result: Dictionary of account information after successful login.
@@ -354,7 +342,6 @@ class LoginRestServlet(RestServlet):
             initial_display_name,
             auth_provider_id=auth_provider_id,
             should_issue_refresh_token=should_issue_refresh_token,
-            auth_provider_session_id=auth_provider_session_id,
         )
 
         result = LoginResponse(
@@ -400,7 +387,6 @@ class LoginRestServlet(RestServlet):
             self.auth_handler._sso_login_callback,
             auth_provider_id=res.auth_provider_id,
             should_issue_refresh_token=should_issue_refresh_token,
-            auth_provider_session_id=res.auth_provider_session_id,
         )
 
     async def _do_jwt_login(
@@ -462,7 +448,9 @@ def _get_auth_flow_dict_for_idp(idp: SsoIdentityProvider) -> JsonDict:
 
 
 class RefreshTokenServlet(RestServlet):
-    PATTERNS = (re.compile("^/_matrix/client/v1/refresh$"),)
+    PATTERNS = client_patterns(
+        "/org.matrix.msc2918.refresh_token/refresh$", releases=(), unstable=True
+    )
 
     def __init__(self, hs: "HomeServer"):
         self._auth_handler = hs.get_auth_handler()
@@ -470,7 +458,6 @@ class RefreshTokenServlet(RestServlet):
         self.refreshable_access_token_lifetime = (
             hs.config.registration.refreshable_access_token_lifetime
         )
-        self.refresh_token_lifetime = hs.config.registration.refresh_token_lifetime
 
     async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
         refresh_submission = parse_json_object_from_request(request)
@@ -480,32 +467,21 @@ class RefreshTokenServlet(RestServlet):
         if not isinstance(token, str):
             raise SynapseError(400, "Invalid param: refresh_token", Codes.INVALID_PARAM)
 
-        now = self._clock.time_msec()
-        access_valid_until_ms = None
-        if self.refreshable_access_token_lifetime is not None:
-            access_valid_until_ms = now + self.refreshable_access_token_lifetime
-        refresh_valid_until_ms = None
-        if self.refresh_token_lifetime is not None:
-            refresh_valid_until_ms = now + self.refresh_token_lifetime
-
-        (
-            access_token,
-            refresh_token,
-            actual_access_token_expiry,
-        ) = await self._auth_handler.refresh_token(
-            token, access_valid_until_ms, refresh_valid_until_ms
+        valid_until_ms = (
+            self._clock.time_msec() + self.refreshable_access_token_lifetime
+        )
+        access_token, refresh_token = await self._auth_handler.refresh_token(
+            token, valid_until_ms
+        )
+        expires_in_ms = valid_until_ms - self._clock.time_msec()
+        return (
+            200,
+            {
+                "access_token": access_token,
+                "refresh_token": refresh_token,
+                "expires_in_ms": expires_in_ms,
+            },
         )
-
-        response: Dict[str, Union[str, int]] = {
-            "access_token": access_token,
-            "refresh_token": refresh_token,
-        }
-
-        # expires_in_ms is only present if the token expires
-        if actual_access_token_expiry is not None:
-            response["expires_in_ms"] = actual_access_token_expiry - now
-
-        return 200, response
 
 
 class SsoRedirectServlet(RestServlet):
@@ -513,7 +489,7 @@ class SsoRedirectServlet(RestServlet):
         re.compile(
             "^"
             + CLIENT_API_PREFIX
-            + "/(r0|v3)/login/sso/redirect/(?P<idp_id>[A-Za-z0-9_.~-]+)$"
+            + "/r0/login/sso/redirect/(?P<idp_id>[A-Za-z0-9_.~-]+)$"
         )
     ]
 
diff --git a/synapse/rest/client/register.py b/synapse/rest/client/register.py
index 8b56c76aed..d2b11e39d9 100644
--- a/synapse/rest/client/register.py
+++ b/synapse/rest/client/register.py
@@ -41,6 +41,7 @@ from synapse.http.server import HttpServer, finish_request, respond_with_html
 from synapse.http.servlet import (
     RestServlet,
     assert_params_in_dict,
+    parse_boolean,
     parse_json_object_from_request,
     parse_string,
 )
@@ -419,7 +420,7 @@ class RegisterRestServlet(RestServlet):
         self.password_policy_handler = hs.get_password_policy_handler()
         self.clock = hs.get_clock()
         self._registration_enabled = self.hs.config.registration.enable_registration
-        self._refresh_tokens_enabled = (
+        self._msc2918_enabled = (
             hs.config.registration.refreshable_access_token_lifetime is not None
         )
 
@@ -445,15 +446,14 @@ class RegisterRestServlet(RestServlet):
                 f"Do not understand membership kind: {kind}",
             )
 
-        # Check if the clients wishes for this registration to issue a refresh
-        # token.
-        client_requested_refresh_tokens = body.get("refresh_token", False)
-        if not isinstance(client_requested_refresh_tokens, bool):
-            raise SynapseError(400, "`refresh_token` should be true or false.")
-
-        should_issue_refresh_token = (
-            self._refresh_tokens_enabled and client_requested_refresh_tokens
-        )
+        if self._msc2918_enabled:
+            # Check if this registration should also issue a refresh token, as
+            # per MSC2918
+            should_issue_refresh_token = parse_boolean(
+                request, name="org.matrix.msc2918.refresh_token", default=False
+            )
+        else:
+            should_issue_refresh_token = False
 
         # Pull out the provided username and do basic sanity checks early since
         # the auth layer will store these in sessions.
diff --git a/synapse/rest/client/relations.py b/synapse/rest/client/relations.py
index fc4e6921c5..45e9f1dd90 100644
--- a/synapse/rest/client/relations.py
+++ b/synapse/rest/client/relations.py
@@ -224,14 +224,18 @@ class RelationPaginationServlet(RestServlet):
         )
 
         now = self.clock.time_msec()
-        # Do not bundle aggregations when retrieving the original event because
-        # we want the content before relations are applied to it.
+        # We set bundle_relations to False when retrieving the original
+        # event because we want the content before relations were applied to
+        # it.
         original_event = await self._event_serializer.serialize_event(
-            event, now, bundle_aggregations=False
+            event, now, bundle_relations=False
+        )
+        # Similarly, we don't allow relations to be applied to relations, so we
+        # return the original relations without any aggregations on top of them
+        # here.
+        serialized_events = await self._event_serializer.serialize_events(
+            events, now, bundle_relations=False
         )
-        # The relations returned for the requested event do include their
-        # bundled aggregations.
-        serialized_events = await self._event_serializer.serialize_events(events, now)
 
         return_value = pagination_chunk.to_dict()
         return_value["chunk"] = serialized_events
diff --git a/synapse/rest/client/room.py b/synapse/rest/client/room.py
index f48e2e6ca2..955d4e8641 100644
--- a/synapse/rest/client/room.py
+++ b/synapse/rest/client/room.py
@@ -716,7 +716,10 @@ class RoomEventContextServlet(RestServlet):
             results["events_after"], time_now
         )
         results["state"] = await self._event_serializer.serialize_events(
-            results["state"], time_now
+            results["state"],
+            time_now,
+            # No need to bundle aggregations for state events
+            bundle_relations=False,
         )
 
         return 200, results
@@ -1067,62 +1070,6 @@ def register_txn_path(
         )
 
 
-class TimestampLookupRestServlet(RestServlet):
-    """
-    API endpoint to fetch the `event_id` of the closest event to the given
-    timestamp (`ts` query parameter) in the given direction (`dir` query
-    parameter).
-
-    Useful for cases like jump to date so you can start paginating messages from
-    a given date in the archive.
-
-    `ts` is a timestamp in milliseconds where we will find the closest event in
-    the given direction.
-
-    `dir` can be `f` or `b` to indicate forwards and backwards in time from the
-    given timestamp.
-
-    GET /_matrix/client/unstable/org.matrix.msc3030/rooms/<roomID>/timestamp_to_event?ts=<timestamp>&dir=<direction>
-    {
-        "event_id": ...
-    }
-    """
-
-    PATTERNS = (
-        re.compile(
-            "^/_matrix/client/unstable/org.matrix.msc3030"
-            "/rooms/(?P<room_id>[^/]*)/timestamp_to_event$"
-        ),
-    )
-
-    def __init__(self, hs: "HomeServer"):
-        super().__init__()
-        self._auth = hs.get_auth()
-        self._store = hs.get_datastore()
-        self.timestamp_lookup_handler = hs.get_timestamp_lookup_handler()
-
-    async def on_GET(
-        self, request: SynapseRequest, room_id: str
-    ) -> Tuple[int, JsonDict]:
-        requester = await self._auth.get_user_by_req(request)
-        await self._auth.check_user_in_room(room_id, requester.user.to_string())
-
-        timestamp = parse_integer(request, "ts", required=True)
-        direction = parse_string(request, "dir", default="f", allowed_values=["f", "b"])
-
-        (
-            event_id,
-            origin_server_ts,
-        ) = await self.timestamp_lookup_handler.get_event_for_timestamp(
-            requester, room_id, timestamp, direction
-        )
-
-        return 200, {
-            "event_id": event_id,
-            "origin_server_ts": origin_server_ts,
-        }
-
-
 class RoomSpaceSummaryRestServlet(RestServlet):
     PATTERNS = (
         re.compile(
@@ -1193,7 +1140,7 @@ class RoomSpaceSummaryRestServlet(RestServlet):
 class RoomHierarchyRestServlet(RestServlet):
     PATTERNS = (
         re.compile(
-            "^/_matrix/client/(v1|unstable/org.matrix.msc2946)"
+            "^/_matrix/client/unstable/org.matrix.msc2946"
             "/rooms/(?P<room_id>[^/]*)/hierarchy$"
         ),
     )
@@ -1221,7 +1168,7 @@ class RoomHierarchyRestServlet(RestServlet):
             )
 
         return 200, await self._room_summary_handler.get_room_hierarchy(
-            requester,
+            requester.user.to_string(),
             room_id,
             suggested_only=parse_boolean(request, "suggested_only", default=False),
             max_depth=max_depth,
@@ -1292,8 +1239,6 @@ def register_servlets(
     RoomAliasListServlet(hs).register(http_server)
     SearchRestServlet(hs).register(http_server)
     RoomCreateRestServlet(hs).register(http_server)
-    if hs.config.experimental.msc3030_enabled:
-        TimestampLookupRestServlet(hs).register(http_server)
 
     # Some servlets only get registered for the main process.
     if not is_worker:
diff --git a/synapse/rest/client/sync.py b/synapse/rest/client/sync.py
index 88e4f5e063..b6a2485732 100644
--- a/synapse/rest/client/sync.py
+++ b/synapse/rest/client/sync.py
@@ -520,9 +520,9 @@ class SyncRestServlet(RestServlet):
             return self._event_serializer.serialize_events(
                 events,
                 time_now=time_now,
-                # Don't bother to bundle aggregations if the timeline is unlimited,
-                # as clients will have all the necessary information.
-                bundle_aggregations=room.timeline.limited,
+                # We don't bundle "live" events, as otherwise clients
+                # will end up double counting annotations.
+                bundle_relations=False,
                 token_id=token_id,
                 event_format=event_formatter,
                 only_event_fields=only_fields,
diff --git a/synapse/rest/media/v1/filepath.py b/synapse/rest/media/v1/filepath.py
index 1f6441c412..c0e15c6513 100644
--- a/synapse/rest/media/v1/filepath.py
+++ b/synapse/rest/media/v1/filepath.py
@@ -43,75 +43,47 @@ GetPathMethod = TypeVar(
 )
 
 
-def _wrap_with_jail_check(relative: bool) -> Callable[[GetPathMethod], GetPathMethod]:
+def _wrap_with_jail_check(func: GetPathMethod) -> GetPathMethod:
     """Wraps a path-returning method to check that the returned path(s) do not escape
     the media store directory.
 
-    The path-returning method may return either a single path, or a list of paths.
-
     The check is not expected to ever fail, unless `func` is missing a call to
     `_validate_path_component`, or `_validate_path_component` is buggy.
 
     Args:
-        relative: A boolean indicating whether the wrapped method returns paths relative
-            to the media store directory.
+        func: The `MediaFilePaths` method to wrap. The method may return either a single
+            path, or a list of paths. Returned paths may be either absolute or relative.
 
     Returns:
-        A method which will wrap a path-returning method, adding a check to ensure that
-        the returned path(s) lie within the media store directory. The check will raise
-        a `ValueError` if it fails.
+        The method, wrapped with a check to ensure that the returned path(s) lie within
+        the media store directory. Raises a `ValueError` if the check fails.
     """
 
-    def _wrap_with_jail_check_inner(func: GetPathMethod) -> GetPathMethod:
-        @functools.wraps(func)
-        def _wrapped(
-            self: "MediaFilePaths", *args: Any, **kwargs: Any
-        ) -> Union[str, List[str]]:
-            path_or_paths = func(self, *args, **kwargs)
-
-            if isinstance(path_or_paths, list):
-                paths_to_check = path_or_paths
-            else:
-                paths_to_check = [path_or_paths]
-
-            for path in paths_to_check:
-                # Construct the path that will ultimately be used.
-                # We cannot guess whether `path` is relative to the media store
-                # directory, since the media store directory may itself be a relative
-                # path.
-                if relative:
-                    path = os.path.join(self.base_path, path)
-                normalized_path = os.path.normpath(path)
-
-                # Now that `normpath` has eliminated `../`s and `./`s from the path,
-                # `os.path.commonpath` can be used to check whether it lies within the
-                # media store directory.
-                if (
-                    os.path.commonpath([normalized_path, self.normalized_base_path])
-                    != self.normalized_base_path
-                ):
-                    # The path resolves to outside the media store directory,
-                    # or `self.base_path` is `.`, which is an unlikely configuration.
-                    raise ValueError(f"Invalid media store path: {path!r}")
-
-                # Note that `os.path.normpath`/`abspath` has a subtle caveat:
-                # `a/b/c/../c` will normalize to `a/b/c`, but the former refers to a
-                # different path if `a/b/c` is a symlink. That is, the check above is
-                # not perfect and may allow a certain restricted subset of untrustworthy
-                # paths through. Since the check above is secondary to the main
-                # `_validate_path_component` checks, it's less important for it to be
-                # perfect.
-                #
-                # As an alternative, `os.path.realpath` will resolve symlinks, but
-                # proves problematic if there are symlinks inside the media store.
-                # eg. if `url_store/` is symlinked to elsewhere, its canonical path
-                # won't match that of the main media store directory.
-
-            return path_or_paths
-
-        return cast(GetPathMethod, _wrapped)
-
-    return _wrap_with_jail_check_inner
+    @functools.wraps(func)
+    def _wrapped(
+        self: "MediaFilePaths", *args: Any, **kwargs: Any
+    ) -> Union[str, List[str]]:
+        path_or_paths = func(self, *args, **kwargs)
+
+        if isinstance(path_or_paths, list):
+            paths_to_check = path_or_paths
+        else:
+            paths_to_check = [path_or_paths]
+
+        for path in paths_to_check:
+            # path may be an absolute or relative path, depending on the method being
+            # wrapped. When "appending" an absolute path, `os.path.join` discards the
+            # previous path, which is desired here.
+            normalized_path = os.path.normpath(os.path.join(self.real_base_path, path))
+            if (
+                os.path.commonpath([normalized_path, self.real_base_path])
+                != self.real_base_path
+            ):
+                raise ValueError(f"Invalid media store path: {path!r}")
+
+        return path_or_paths
+
+    return cast(GetPathMethod, _wrapped)
 
 
 ALLOWED_CHARACTERS = set(
@@ -155,7 +127,9 @@ class MediaFilePaths:
 
     def __init__(self, primary_base_path: str):
         self.base_path = primary_base_path
-        self.normalized_base_path = os.path.normpath(self.base_path)
+
+        # The media store directory, with all symlinks resolved.
+        self.real_base_path = os.path.realpath(primary_base_path)
 
         # Refuse to initialize if paths cannot be validated correctly for the current
         # platform.
@@ -166,7 +140,7 @@ class MediaFilePaths:
         # for certain homeservers there, since ":"s aren't allowed in paths.
         assert os.name == "posix"
 
-    @_wrap_with_jail_check(relative=True)
+    @_wrap_with_jail_check
     def local_media_filepath_rel(self, media_id: str) -> str:
         return os.path.join(
             "local_content",
@@ -177,7 +151,7 @@ class MediaFilePaths:
 
     local_media_filepath = _wrap_in_base_path(local_media_filepath_rel)
 
-    @_wrap_with_jail_check(relative=True)
+    @_wrap_with_jail_check
     def local_media_thumbnail_rel(
         self, media_id: str, width: int, height: int, content_type: str, method: str
     ) -> str:
@@ -193,7 +167,7 @@ class MediaFilePaths:
 
     local_media_thumbnail = _wrap_in_base_path(local_media_thumbnail_rel)
 
-    @_wrap_with_jail_check(relative=False)
+    @_wrap_with_jail_check
     def local_media_thumbnail_dir(self, media_id: str) -> str:
         """
         Retrieve the local store path of thumbnails of a given media_id
@@ -211,7 +185,7 @@ class MediaFilePaths:
             _validate_path_component(media_id[4:]),
         )
 
-    @_wrap_with_jail_check(relative=True)
+    @_wrap_with_jail_check
     def remote_media_filepath_rel(self, server_name: str, file_id: str) -> str:
         return os.path.join(
             "remote_content",
@@ -223,7 +197,7 @@ class MediaFilePaths:
 
     remote_media_filepath = _wrap_in_base_path(remote_media_filepath_rel)
 
-    @_wrap_with_jail_check(relative=True)
+    @_wrap_with_jail_check
     def remote_media_thumbnail_rel(
         self,
         server_name: str,
@@ -249,7 +223,7 @@ class MediaFilePaths:
     # Legacy path that was used to store thumbnails previously.
     # Should be removed after some time, when most of the thumbnails are stored
     # using the new path.
-    @_wrap_with_jail_check(relative=True)
+    @_wrap_with_jail_check
     def remote_media_thumbnail_rel_legacy(
         self, server_name: str, file_id: str, width: int, height: int, content_type: str
     ) -> str:
@@ -264,7 +238,6 @@ class MediaFilePaths:
             _validate_path_component(file_name),
         )
 
-    @_wrap_with_jail_check(relative=False)
     def remote_media_thumbnail_dir(self, server_name: str, file_id: str) -> str:
         return os.path.join(
             self.base_path,
@@ -275,7 +248,7 @@ class MediaFilePaths:
             _validate_path_component(file_id[4:]),
         )
 
-    @_wrap_with_jail_check(relative=True)
+    @_wrap_with_jail_check
     def url_cache_filepath_rel(self, media_id: str) -> str:
         if NEW_FORMAT_ID_RE.match(media_id):
             # Media id is of the form <DATE><RANDOM_STRING>
@@ -295,7 +268,7 @@ class MediaFilePaths:
 
     url_cache_filepath = _wrap_in_base_path(url_cache_filepath_rel)
 
-    @_wrap_with_jail_check(relative=False)
+    @_wrap_with_jail_check
     def url_cache_filepath_dirs_to_delete(self, media_id: str) -> List[str]:
         "The dirs to try and remove if we delete the media_id file"
         if NEW_FORMAT_ID_RE.match(media_id):
@@ -317,7 +290,7 @@ class MediaFilePaths:
                 ),
             ]
 
-    @_wrap_with_jail_check(relative=True)
+    @_wrap_with_jail_check
     def url_cache_thumbnail_rel(
         self, media_id: str, width: int, height: int, content_type: str, method: str
     ) -> str:
@@ -345,7 +318,7 @@ class MediaFilePaths:
 
     url_cache_thumbnail = _wrap_in_base_path(url_cache_thumbnail_rel)
 
-    @_wrap_with_jail_check(relative=True)
+    @_wrap_with_jail_check
     def url_cache_thumbnail_directory_rel(self, media_id: str) -> str:
         # Media id is of the form <DATE><RANDOM_STRING>
         # E.g.: 2017-09-28-fsdRDt24DS234dsf
@@ -368,7 +341,7 @@ class MediaFilePaths:
         url_cache_thumbnail_directory_rel
     )
 
-    @_wrap_with_jail_check(relative=False)
+    @_wrap_with_jail_check
     def url_cache_thumbnail_dirs_to_delete(self, media_id: str) -> List[str]:
         "The dirs to try and remove if we delete the media_id thumbnails"
         # Media id is of the form <DATE><RANDOM_STRING>