diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py
index 1b72727b75..c21b7eb37e 100644
--- a/synapse/config/experimental.py
+++ b/synapse/config/experimental.py
@@ -437,10 +437,6 @@ class ExperimentalConfig(Config):
"msc3823_account_suspension", False
)
- self.msc3916_authenticated_media_enabled = experimental.get(
- "msc3916_authenticated_media_enabled", False
- )
-
# MSC4151: Report room API (Client-Server API)
self.msc4151_enabled: bool = experimental.get("msc4151_enabled", False)
diff --git a/synapse/federation/transport/server/__init__.py b/synapse/federation/transport/server/__init__.py
index c44e5daa47..5f997040d0 100644
--- a/synapse/federation/transport/server/__init__.py
+++ b/synapse/federation/transport/server/__init__.py
@@ -33,6 +33,7 @@ from synapse.federation.transport.server.federation import (
FEDERATION_SERVLET_CLASSES,
FederationAccountStatusServlet,
FederationMediaDownloadServlet,
+ FederationMediaThumbnailServlet,
FederationUnstableClientKeysClaimServlet,
)
from synapse.http.server import HttpServer, JsonResource
@@ -316,7 +317,10 @@ def register_servlets(
):
continue
- if servletclass == FederationMediaDownloadServlet:
+ if (
+ servletclass == FederationMediaDownloadServlet
+ or servletclass == FederationMediaThumbnailServlet
+ ):
if not hs.config.server.enable_media_repo:
continue
diff --git a/synapse/federation/transport/server/_base.py b/synapse/federation/transport/server/_base.py
index e124481474..9094201da0 100644
--- a/synapse/federation/transport/server/_base.py
+++ b/synapse/federation/transport/server/_base.py
@@ -363,6 +363,8 @@ class BaseFederationServlet:
if (
func.__self__.__class__.__name__ # type: ignore
== "FederationMediaDownloadServlet"
+ or func.__self__.__class__.__name__ # type: ignore
+ == "FederationMediaThumbnailServlet"
):
response = await func(
origin, content, request, *args, **kwargs
@@ -375,6 +377,8 @@ class BaseFederationServlet:
if (
func.__self__.__class__.__name__ # type: ignore
== "FederationMediaDownloadServlet"
+ or func.__self__.__class__.__name__ # type: ignore
+ == "FederationMediaThumbnailServlet"
):
response = await func(
origin, content, request, *args, **kwargs
diff --git a/synapse/federation/transport/server/federation.py b/synapse/federation/transport/server/federation.py
index ec957768d4..b075a86f68 100644
--- a/synapse/federation/transport/server/federation.py
+++ b/synapse/federation/transport/server/federation.py
@@ -46,11 +46,13 @@ from synapse.http.servlet import (
parse_boolean_from_args,
parse_integer,
parse_integer_from_args,
+ parse_string,
parse_string_from_args,
parse_strings_from_args,
)
from synapse.http.site import SynapseRequest
from synapse.media._base import DEFAULT_MAX_TIMEOUT_MS, MAXIMUM_ALLOWED_MAX_TIMEOUT_MS
+from synapse.media.thumbnailer import ThumbnailProvider
from synapse.types import JsonDict
from synapse.util import SYNAPSE_VERSION
from synapse.util.ratelimitutils import FederationRateLimiter
@@ -826,6 +828,59 @@ class FederationMediaDownloadServlet(BaseFederationServerServlet):
)
+class FederationMediaThumbnailServlet(BaseFederationServerServlet):
+ """
+ Implementation of new federation media `/thumbnail` endpoint outlined in MSC3916. Returns
+ a multipart/mixed response consisting of a JSON object and the requested media
+ item. This endpoint only returns local media.
+ """
+
+ PATH = "/media/thumbnail/(?P<media_id>[^/]*)"
+ RATELIMIT = True
+
+ def __init__(
+ self,
+ hs: "HomeServer",
+ ratelimiter: FederationRateLimiter,
+ authenticator: Authenticator,
+ server_name: str,
+ ):
+ super().__init__(hs, authenticator, ratelimiter, server_name)
+ self.media_repo = self.hs.get_media_repository()
+ self.dynamic_thumbnails = hs.config.media.dynamic_thumbnails
+ self.thumbnail_provider = ThumbnailProvider(
+ hs, self.media_repo, self.media_repo.media_storage
+ )
+
+ async def on_GET(
+ self,
+ origin: Optional[str],
+ content: Literal[None],
+ request: SynapseRequest,
+ media_id: str,
+ ) -> None:
+
+ width = parse_integer(request, "width", required=True)
+ height = parse_integer(request, "height", required=True)
+ method = parse_string(request, "method", "scale")
+ # TODO Parse the Accept header to get an prioritised list of thumbnail types.
+ m_type = "image/png"
+ max_timeout_ms = parse_integer(
+ request, "timeout_ms", default=DEFAULT_MAX_TIMEOUT_MS
+ )
+ max_timeout_ms = min(max_timeout_ms, MAXIMUM_ALLOWED_MAX_TIMEOUT_MS)
+
+ if self.dynamic_thumbnails:
+ await self.thumbnail_provider.select_or_generate_local_thumbnail(
+ request, media_id, width, height, method, m_type, max_timeout_ms, True
+ )
+ else:
+ await self.thumbnail_provider.respond_local_thumbnail(
+ request, media_id, width, height, method, m_type, max_timeout_ms, True
+ )
+ self.media_repo.mark_recently_accessed(None, media_id)
+
+
FEDERATION_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = (
FederationSendServlet,
FederationEventServlet,
@@ -858,4 +913,5 @@ FEDERATION_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = (
FederationMakeKnockServlet,
FederationAccountStatusServlet,
FederationMediaDownloadServlet,
+ FederationMediaThumbnailServlet,
)
diff --git a/synapse/media/media_repository.py b/synapse/media/media_repository.py
index 542642b900..87c929eb20 100644
--- a/synapse/media/media_repository.py
+++ b/synapse/media/media_repository.py
@@ -542,7 +542,12 @@ class MediaRepository:
respond_404(request)
async def get_remote_media_info(
- self, server_name: str, media_id: str, max_timeout_ms: int, ip_address: str
+ self,
+ server_name: str,
+ media_id: str,
+ max_timeout_ms: int,
+ ip_address: str,
+ use_federation: bool,
) -> RemoteMedia:
"""Gets the media info associated with the remote file, downloading
if necessary.
@@ -553,6 +558,8 @@ class MediaRepository:
max_timeout_ms: the maximum number of milliseconds to wait for the
media to be uploaded.
ip_address: IP address of the requester
+ use_federation: if a download is necessary, whether to request the remote file
+ over the federation `/download` endpoint
Returns:
The media info of the file
@@ -573,7 +580,7 @@ class MediaRepository:
max_timeout_ms,
self.download_ratelimiter,
ip_address,
- False,
+ use_federation,
)
# Ensure we actually use the responder so that it releases resources
diff --git a/synapse/media/thumbnailer.py b/synapse/media/thumbnailer.py
index f8a9560784..413a720e40 100644
--- a/synapse/media/thumbnailer.py
+++ b/synapse/media/thumbnailer.py
@@ -36,9 +36,11 @@ from synapse.media._base import (
ThumbnailInfo,
respond_404,
respond_with_file,
+ respond_with_multipart_responder,
respond_with_responder,
)
-from synapse.media.media_storage import MediaStorage
+from synapse.media.media_storage import FileResponder, MediaStorage
+from synapse.storage.databases.main.media_repository import LocalMedia
if TYPE_CHECKING:
from synapse.media.media_repository import MediaRepository
@@ -271,6 +273,7 @@ class ThumbnailProvider:
method: str,
m_type: str,
max_timeout_ms: int,
+ for_federation: bool,
) -> None:
media_info = await self.media_repo.get_local_media_info(
request, media_id, max_timeout_ms
@@ -290,6 +293,8 @@ class ThumbnailProvider:
media_id,
url_cache=bool(media_info.url_cache),
server_name=None,
+ for_federation=for_federation,
+ media_info=media_info,
)
async def select_or_generate_local_thumbnail(
@@ -301,6 +306,7 @@ class ThumbnailProvider:
desired_method: str,
desired_type: str,
max_timeout_ms: int,
+ for_federation: bool,
) -> None:
media_info = await self.media_repo.get_local_media_info(
request, media_id, max_timeout_ms
@@ -326,10 +332,16 @@ class ThumbnailProvider:
responder = await self.media_storage.fetch_media(file_info)
if responder:
- await respond_with_responder(
- request, responder, info.type, info.length
- )
- return
+ if for_federation:
+ await respond_with_multipart_responder(
+ self.hs.get_clock(), request, responder, media_info
+ )
+ return
+ else:
+ await respond_with_responder(
+ request, responder, info.type, info.length
+ )
+ return
logger.debug("We don't have a thumbnail of that size. Generating")
@@ -344,7 +356,15 @@ class ThumbnailProvider:
)
if file_path:
- await respond_with_file(request, desired_type, file_path)
+ if for_federation:
+ await respond_with_multipart_responder(
+ self.hs.get_clock(),
+ request,
+ FileResponder(open(file_path, "rb")),
+ media_info,
+ )
+ else:
+ await respond_with_file(request, desired_type, file_path)
else:
logger.warning("Failed to generate thumbnail")
raise SynapseError(400, "Failed to generate thumbnail.")
@@ -360,9 +380,10 @@ class ThumbnailProvider:
desired_type: str,
max_timeout_ms: int,
ip_address: str,
+ use_federation: bool,
) -> None:
media_info = await self.media_repo.get_remote_media_info(
- server_name, media_id, max_timeout_ms, ip_address
+ server_name, media_id, max_timeout_ms, ip_address, use_federation
)
if not media_info:
respond_404(request)
@@ -424,12 +445,13 @@ class ThumbnailProvider:
m_type: str,
max_timeout_ms: int,
ip_address: str,
+ use_federation: bool,
) -> None:
# TODO: Don't download the whole remote file
# We should proxy the thumbnail from the remote server instead of
# downloading the remote file and generating our own thumbnails.
media_info = await self.media_repo.get_remote_media_info(
- server_name, media_id, max_timeout_ms, ip_address
+ server_name, media_id, max_timeout_ms, ip_address, use_federation
)
if not media_info:
return
@@ -448,6 +470,7 @@ class ThumbnailProvider:
media_info.filesystem_id,
url_cache=False,
server_name=server_name,
+ for_federation=False,
)
async def _select_and_respond_with_thumbnail(
@@ -461,7 +484,9 @@ class ThumbnailProvider:
media_id: str,
file_id: str,
url_cache: bool,
+ for_federation: bool,
server_name: Optional[str] = None,
+ media_info: Optional[LocalMedia] = None,
) -> None:
"""
Respond to a request with an appropriate thumbnail from the previously generated thumbnails.
@@ -476,6 +501,8 @@ class ThumbnailProvider:
file_id: The ID of the media that a thumbnail is being requested for.
url_cache: True if this is from a URL cache.
server_name: The server name, if this is a remote thumbnail.
+ for_federation: whether the request is from the federation /thumbnail request
+ media_info: metadata about the media being requested.
"""
logger.debug(
"_select_and_respond_with_thumbnail: media_id=%s desired=%sx%s (%s) thumbnail_infos=%s",
@@ -511,13 +538,20 @@ class ThumbnailProvider:
responder = await self.media_storage.fetch_media(file_info)
if responder:
- await respond_with_responder(
- request,
- responder,
- file_info.thumbnail.type,
- file_info.thumbnail.length,
- )
- return
+ if for_federation:
+ assert media_info is not None
+ await respond_with_multipart_responder(
+ self.hs.get_clock(), request, responder, media_info
+ )
+ return
+ else:
+ await respond_with_responder(
+ request,
+ responder,
+ file_info.thumbnail.type,
+ file_info.thumbnail.length,
+ )
+ return
# If we can't find the thumbnail we regenerate it. This can happen
# if e.g. we've deleted the thumbnails but still have the original
@@ -558,12 +592,18 @@ class ThumbnailProvider:
)
responder = await self.media_storage.fetch_media(file_info)
- await respond_with_responder(
- request,
- responder,
- file_info.thumbnail.type,
- file_info.thumbnail.length,
- )
+ if for_federation:
+ assert media_info is not None
+ await respond_with_multipart_responder(
+ self.hs.get_clock(), request, responder, media_info
+ )
+ else:
+ await respond_with_responder(
+ request,
+ responder,
+ file_info.thumbnail.type,
+ file_info.thumbnail.length,
+ )
else:
# This might be because:
# 1. We can't create thumbnails for the given media (corrupted or
diff --git a/synapse/rest/client/media.py b/synapse/rest/client/media.py
index c0ae5dd66f..c30e3022de 100644
--- a/synapse/rest/client/media.py
+++ b/synapse/rest/client/media.py
@@ -47,7 +47,7 @@ from synapse.util.stringutils import parse_and_validate_server_name
logger = logging.getLogger(__name__)
-class UnstablePreviewURLServlet(RestServlet):
+class PreviewURLServlet(RestServlet):
"""
Same as `GET /_matrix/media/r0/preview_url`, this endpoint provides a generic preview API
for URLs which outputs Open Graph (https://ogp.me/) responses (with some Matrix
@@ -65,9 +65,7 @@ class UnstablePreviewURLServlet(RestServlet):
* Matrix cannot be used to distribute the metadata between homeservers.
"""
- PATTERNS = [
- re.compile(r"^/_matrix/client/unstable/org.matrix.msc3916/media/preview_url$")
- ]
+ PATTERNS = [re.compile(r"^/_matrix/client/v1/media/preview_url$")]
def __init__(
self,
@@ -95,10 +93,8 @@ class UnstablePreviewURLServlet(RestServlet):
respond_with_json_bytes(request, 200, og, send_cors=True)
-class UnstableMediaConfigResource(RestServlet):
- PATTERNS = [
- re.compile(r"^/_matrix/client/unstable/org.matrix.msc3916/media/config$")
- ]
+class MediaConfigResource(RestServlet):
+ PATTERNS = [re.compile(r"^/_matrix/client/v1/media/config$")]
def __init__(self, hs: "HomeServer"):
super().__init__()
@@ -112,10 +108,10 @@ class UnstableMediaConfigResource(RestServlet):
respond_with_json(request, 200, self.limits_dict, send_cors=True)
-class UnstableThumbnailResource(RestServlet):
+class ThumbnailResource(RestServlet):
PATTERNS = [
re.compile(
- "/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/(?P<server_name>[^/]*)/(?P<media_id>[^/]*)$"
+ "/_matrix/client/v1/media/thumbnail/(?P<server_name>[^/]*)/(?P<media_id>[^/]*)$"
)
]
@@ -159,11 +155,25 @@ class UnstableThumbnailResource(RestServlet):
if self._is_mine_server_name(server_name):
if self.dynamic_thumbnails:
await self.thumbnailer.select_or_generate_local_thumbnail(
- request, media_id, width, height, method, m_type, max_timeout_ms
+ request,
+ media_id,
+ width,
+ height,
+ method,
+ m_type,
+ max_timeout_ms,
+ False,
)
else:
await self.thumbnailer.respond_local_thumbnail(
- request, media_id, width, height, method, m_type, max_timeout_ms
+ request,
+ media_id,
+ width,
+ height,
+ method,
+ m_type,
+ max_timeout_ms,
+ False,
)
self.media_repo.mark_recently_accessed(None, media_id)
else:
@@ -191,6 +201,7 @@ class UnstableThumbnailResource(RestServlet):
m_type,
max_timeout_ms,
ip_address,
+ True,
)
self.media_repo.mark_recently_accessed(server_name, media_id)
@@ -260,11 +271,9 @@ class DownloadResource(RestServlet):
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
media_repo = hs.get_media_repository()
if hs.config.media.url_preview_enabled:
- UnstablePreviewURLServlet(hs, media_repo, media_repo.media_storage).register(
+ PreviewURLServlet(hs, media_repo, media_repo.media_storage).register(
http_server
)
- UnstableMediaConfigResource(hs).register(http_server)
- UnstableThumbnailResource(hs, media_repo, media_repo.media_storage).register(
- http_server
- )
+ MediaConfigResource(hs).register(http_server)
+ ThumbnailResource(hs, media_repo, media_repo.media_storage).register(http_server)
DownloadResource(hs, media_repo).register(http_server)
diff --git a/synapse/rest/media/thumbnail_resource.py b/synapse/rest/media/thumbnail_resource.py
index ce511c6dce..70354aa439 100644
--- a/synapse/rest/media/thumbnail_resource.py
+++ b/synapse/rest/media/thumbnail_resource.py
@@ -88,11 +88,25 @@ class ThumbnailResource(RestServlet):
if self._is_mine_server_name(server_name):
if self.dynamic_thumbnails:
await self.thumbnail_provider.select_or_generate_local_thumbnail(
- request, media_id, width, height, method, m_type, max_timeout_ms
+ request,
+ media_id,
+ width,
+ height,
+ method,
+ m_type,
+ max_timeout_ms,
+ False,
)
else:
await self.thumbnail_provider.respond_local_thumbnail(
- request, media_id, width, height, method, m_type, max_timeout_ms
+ request,
+ media_id,
+ width,
+ height,
+ method,
+ m_type,
+ max_timeout_ms,
+ False,
)
self.media_repo.mark_recently_accessed(None, media_id)
else:
@@ -120,5 +134,6 @@ class ThumbnailResource(RestServlet):
m_type,
max_timeout_ms,
ip_address,
+ False,
)
self.media_repo.mark_recently_accessed(server_name, media_id)
|