diff --git a/changelog.d/17388.feature b/changelog.d/17388.feature
new file mode 100644
index 0000000000..f04f49f085
--- /dev/null
+++ b/changelog.d/17388.feature
@@ -0,0 +1,3 @@
+Support [MSC3916](https://github.com/matrix-org/matrix-spec-proposals/blob/rav/authentication-for-media/proposals/3916-authentication-for-media.md)
+by adding `_matrix/client/v1/media/thumbnail`, `_matrix/federation/v1/media/thumbnail` endpoints and stabilizing the
+remaining `_matrix/client/v1/media` endpoints.
\ No newline at end of file
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)
diff --git a/tests/federation/test_federation_media.py b/tests/federation/test_federation_media.py
index 142f73cfdb..0dcf20f5f5 100644
--- a/tests/federation/test_federation_media.py
+++ b/tests/federation/test_federation_media.py
@@ -35,6 +35,7 @@ from synapse.types import UserID
from synapse.util import Clock
from tests import unittest
+from tests.media.test_media_storage import small_png
from tests.test_utils import SMALL_PNG
@@ -146,3 +147,112 @@ class FederationMediaDownloadsTest(unittest.FederatingHomeserverTestCase):
# check that the png file exists and matches what was uploaded
found_file = any(SMALL_PNG in field for field in stripped_bytes)
self.assertTrue(found_file)
+
+
+class FederationThumbnailTest(unittest.FederatingHomeserverTestCase):
+
+ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
+ super().prepare(reactor, clock, hs)
+ self.test_dir = tempfile.mkdtemp(prefix="synapse-tests-")
+ self.addCleanup(shutil.rmtree, self.test_dir)
+ self.primary_base_path = os.path.join(self.test_dir, "primary")
+ self.secondary_base_path = os.path.join(self.test_dir, "secondary")
+
+ hs.config.media.media_store_path = self.primary_base_path
+
+ storage_providers = [
+ StorageProviderWrapper(
+ FileStorageProviderBackend(hs, self.secondary_base_path),
+ store_local=True,
+ store_remote=False,
+ store_synchronous=True,
+ )
+ ]
+
+ self.filepaths = MediaFilePaths(self.primary_base_path)
+ self.media_storage = MediaStorage(
+ hs, self.primary_base_path, self.filepaths, storage_providers
+ )
+ self.media_repo = hs.get_media_repository()
+
+ def test_thumbnail_download_scaled(self) -> None:
+ content = io.BytesIO(small_png.data)
+ content_uri = self.get_success(
+ self.media_repo.create_content(
+ "image/png",
+ "test_png_thumbnail",
+ content,
+ 67,
+ UserID.from_string("@user_id:whatever.org"),
+ )
+ )
+ # test with an image file
+ channel = self.make_signed_federation_request(
+ "GET",
+ f"/_matrix/federation/v1/media/thumbnail/{content_uri.media_id}?width=32&height=32&method=scale",
+ )
+ self.pump()
+ self.assertEqual(200, channel.code)
+
+ content_type = channel.headers.getRawHeaders("content-type")
+ assert content_type is not None
+ assert "multipart/mixed" in content_type[0]
+ assert "boundary" in content_type[0]
+
+ # extract boundary
+ boundary = content_type[0].split("boundary=")[1]
+ # split on boundary and check that json field and expected value exist
+ body = channel.result.get("body")
+ assert body is not None
+ stripped_bytes = body.split(b"\r\n" + b"--" + boundary.encode("utf-8"))
+ found_json = any(
+ b"\r\nContent-Type: application/json\r\n\r\n{}" in field
+ for field in stripped_bytes
+ )
+ self.assertTrue(found_json)
+
+ # check that the png file exists and matches the expected scaled bytes
+ found_file = any(small_png.expected_scaled in field for field in stripped_bytes)
+ self.assertTrue(found_file)
+
+ def test_thumbnail_download_cropped(self) -> None:
+ content = io.BytesIO(small_png.data)
+ content_uri = self.get_success(
+ self.media_repo.create_content(
+ "image/png",
+ "test_png_thumbnail",
+ content,
+ 67,
+ UserID.from_string("@user_id:whatever.org"),
+ )
+ )
+ # test with an image file
+ channel = self.make_signed_federation_request(
+ "GET",
+ f"/_matrix/federation/v1/media/thumbnail/{content_uri.media_id}?width=32&height=32&method=crop",
+ )
+ self.pump()
+ self.assertEqual(200, channel.code)
+
+ content_type = channel.headers.getRawHeaders("content-type")
+ assert content_type is not None
+ assert "multipart/mixed" in content_type[0]
+ assert "boundary" in content_type[0]
+
+ # extract boundary
+ boundary = content_type[0].split("boundary=")[1]
+ # split on boundary and check that json field and expected value exist
+ body = channel.result.get("body")
+ assert body is not None
+ stripped_bytes = body.split(b"\r\n" + b"--" + boundary.encode("utf-8"))
+ found_json = any(
+ b"\r\nContent-Type: application/json\r\n\r\n{}" in field
+ for field in stripped_bytes
+ )
+ self.assertTrue(found_json)
+
+ # check that the png file exists and matches the expected cropped bytes
+ found_file = any(
+ small_png.expected_cropped in field for field in stripped_bytes
+ )
+ self.assertTrue(found_file)
diff --git a/tests/media/test_media_storage.py b/tests/media/test_media_storage.py
index 024086b775..70912e22f8 100644
--- a/tests/media/test_media_storage.py
+++ b/tests/media/test_media_storage.py
@@ -18,7 +18,6 @@
# [This file includes modifications made by New Vector Limited]
#
#
-import itertools
import os
import shutil
import tempfile
@@ -227,19 +226,15 @@ test_images = [
empty_file,
SVG,
]
-urls = [
- "_matrix/media/r0/thumbnail",
- "_matrix/client/unstable/org.matrix.msc3916/media/thumbnail",
-]
+input_values = [(x,) for x in test_images]
-@parameterized_class(("test_image", "url"), itertools.product(test_images, urls))
+@parameterized_class(("test_image",), input_values)
class MediaRepoTests(unittest.HomeserverTestCase):
servlets = [media.register_servlets]
test_image: ClassVar[TestImage]
hijack_auth = True
user_id = "@test:user"
- url: ClassVar[str]
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
self.fetches: List[
@@ -304,7 +299,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
"config": {"directory": self.storage_path},
}
config["media_storage_providers"] = [provider_config]
- config["experimental_features"] = {"msc3916_authenticated_media_enabled": True}
hs = self.setup_test_homeserver(config=config, federation_http_client=client)
@@ -509,7 +503,7 @@ class MediaRepoTests(unittest.HomeserverTestCase):
params = "?width=32&height=32&method=scale"
channel = self.make_request(
"GET",
- f"/{self.url}/{self.media_id}{params}",
+ f"/_matrix/media/r0/thumbnail/{self.media_id}{params}",
shorthand=False,
await_result=False,
)
@@ -537,7 +531,7 @@ class MediaRepoTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- f"/{self.url}/{self.media_id}{params}",
+ f"/_matrix/media/r0/thumbnail/{self.media_id}{params}",
shorthand=False,
await_result=False,
)
@@ -573,7 +567,7 @@ class MediaRepoTests(unittest.HomeserverTestCase):
params = "?width=32&height=32&method=" + method
channel = self.make_request(
"GET",
- f"/{self.url}/{self.media_id}{params}",
+ f"/_matrix/media/r0/thumbnail/{self.media_id}{params}",
shorthand=False,
await_result=False,
)
@@ -608,7 +602,7 @@ class MediaRepoTests(unittest.HomeserverTestCase):
channel.json_body,
{
"errcode": "M_UNKNOWN",
- "error": f"Cannot find any thumbnails for the requested media ('/{self.url}/example.com/12345'). This might mean the media is not a supported_media_format=(image/jpeg, image/jpg, image/webp, image/gif, image/png) or that thumbnailing failed for some other reason. (Dynamic thumbnails are disabled on this server.)",
+ "error": "Cannot find any thumbnails for the requested media ('/_matrix/media/r0/thumbnail/example.com/12345'). This might mean the media is not a supported_media_format=(image/jpeg, image/jpg, image/webp, image/gif, image/png) or that thumbnailing failed for some other reason. (Dynamic thumbnails are disabled on this server.)",
},
)
else:
@@ -618,7 +612,7 @@ class MediaRepoTests(unittest.HomeserverTestCase):
channel.json_body,
{
"errcode": "M_NOT_FOUND",
- "error": f"Not found '/{self.url}/example.com/12345'",
+ "error": "Not found '/_matrix/media/r0/thumbnail/example.com/12345'",
},
)
diff --git a/tests/rest/client/test_media.py b/tests/rest/client/test_media.py
index 6b5af2dbb6..7f2caed7d5 100644
--- a/tests/rest/client/test_media.py
+++ b/tests/rest/client/test_media.py
@@ -23,12 +23,15 @@ import io
import json
import os
import re
-from typing import Any, BinaryIO, ClassVar, Dict, List, Optional, Sequence, Tuple, Type
+import shutil
+from typing import Any, BinaryIO, Dict, List, Optional, Sequence, Tuple, Type
from unittest.mock import MagicMock, Mock, patch
from urllib import parse
from urllib.parse import quote, urlencode
-from parameterized import parameterized_class
+from parameterized import parameterized, parameterized_class
+from PIL import Image as Image
+from typing_extensions import ClassVar
from twisted.internet import defer
from twisted.internet._resolver import HostResolution
@@ -40,7 +43,6 @@ from twisted.python.failure import Failure
from twisted.test.proto_helpers import AccumulatingProtocol, MemoryReactor
from twisted.web.http_headers import Headers
from twisted.web.iweb import UNKNOWN_LENGTH, IResponse
-from twisted.web.resource import Resource
from synapse.api.errors import HttpResponseException
from synapse.api.ratelimiting import Ratelimiter
@@ -48,7 +50,8 @@ from synapse.config.oembed import OEmbedEndpointConfig
from synapse.http.client import MultipartResponse
from synapse.http.types import QueryParams
from synapse.logging.context import make_deferred_yieldable
-from synapse.media._base import FileInfo
+from synapse.media._base import FileInfo, ThumbnailInfo
+from synapse.media.thumbnailer import ThumbnailProvider
from synapse.media.url_previewer import IMAGE_CACHE_EXPIRY_MS
from synapse.rest import admin
from synapse.rest.client import login, media
@@ -76,7 +79,7 @@ except ImportError:
lxml = None # type: ignore[assignment]
-class UnstableMediaDomainBlockingTests(unittest.HomeserverTestCase):
+class MediaDomainBlockingTests(unittest.HomeserverTestCase):
remote_media_id = "doesnotmatter"
remote_server_name = "evil.com"
servlets = [
@@ -144,7 +147,6 @@ class UnstableMediaDomainBlockingTests(unittest.HomeserverTestCase):
# Should result in a 404.
"prevent_media_downloads_from": ["evil.com"],
"dynamic_thumbnails": True,
- "experimental_features": {"msc3916_authenticated_media_enabled": True},
}
)
def test_cannot_download_blocked_media_thumbnail(self) -> None:
@@ -153,7 +155,7 @@ class UnstableMediaDomainBlockingTests(unittest.HomeserverTestCase):
"""
response = self.make_request(
"GET",
- f"/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/evil.com/{self.remote_media_id}?width=100&height=100",
+ f"/_matrix/client/v1/media/thumbnail/evil.com/{self.remote_media_id}?width=100&height=100",
shorthand=False,
content={"width": 100, "height": 100},
access_token=self.tok,
@@ -166,7 +168,6 @@ class UnstableMediaDomainBlockingTests(unittest.HomeserverTestCase):
# This proves we haven't broken anything.
"prevent_media_downloads_from": ["not-listed.com"],
"dynamic_thumbnails": True,
- "experimental_features": {"msc3916_authenticated_media_enabled": True},
}
)
def test_remote_media_thumbnail_normally_unblocked(self) -> None:
@@ -175,14 +176,14 @@ class UnstableMediaDomainBlockingTests(unittest.HomeserverTestCase):
"""
response = self.make_request(
"GET",
- f"/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/evil.com/{self.remote_media_id}?width=100&height=100",
+ f"/_matrix/client/v1/media/thumbnail/evil.com/{self.remote_media_id}?width=100&height=100",
shorthand=False,
access_token=self.tok,
)
self.assertEqual(response.code, 200)
-class UnstableURLPreviewTests(unittest.HomeserverTestCase):
+class URLPreviewTests(unittest.HomeserverTestCase):
if not lxml:
skip = "url preview feature requires lxml"
@@ -198,7 +199,6 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
config = self.default_config()
- config["experimental_features"] = {"msc3916_authenticated_media_enabled": True}
config["url_preview_enabled"] = True
config["max_spider_size"] = 9999999
config["url_preview_ip_range_blacklist"] = (
@@ -284,18 +284,6 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
self.reactor.nameResolver = Resolver() # type: ignore[assignment]
- def create_resource_dict(self) -> Dict[str, Resource]:
- """Create a resource tree for the test server
-
- A resource tree is a mapping from path to twisted.web.resource.
-
- The default implementation creates a JsonResource and calls each function in
- `servlets` to register servlets against it.
- """
- resources = super().create_resource_dict()
- resources["/_matrix/media"] = self.hs.get_media_repository_resource()
- return resources
-
def _assert_small_png(self, json_body: JsonDict) -> None:
"""Assert properties from the SMALL_PNG test image."""
self.assertTrue(json_body["og:image"].startswith("mxc://"))
@@ -309,7 +297,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
await_result=False,
)
@@ -334,7 +322,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
# Check the cache returns the correct response
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
)
@@ -352,7 +340,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
# Check the database cache returns the correct response
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
)
@@ -375,7 +363,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
await_result=False,
)
@@ -405,7 +393,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
await_result=False,
)
@@ -441,7 +429,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
await_result=False,
)
@@ -482,7 +470,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
await_result=False,
)
@@ -517,7 +505,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
await_result=False,
)
@@ -550,7 +538,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://example.com",
+ "/_matrix/client/v1/media/preview_url?url=http://example.com",
shorthand=False,
await_result=False,
)
@@ -580,7 +568,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://example.com",
+ "/_matrix/client/v1/media/preview_url?url=http://example.com",
shorthand=False,
)
@@ -603,7 +591,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://example.com",
+ "/_matrix/client/v1/media/preview_url?url=http://example.com",
shorthand=False,
)
@@ -622,7 +610,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
"""
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://192.168.1.1",
+ "/_matrix/client/v1/media/preview_url?url=http://192.168.1.1",
shorthand=False,
)
@@ -640,7 +628,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
"""
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://1.1.1.2",
+ "/_matrix/client/v1/media/preview_url?url=http://1.1.1.2",
shorthand=False,
)
@@ -659,7 +647,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://example.com",
+ "/_matrix/client/v1/media/preview_url?url=http://example.com",
shorthand=False,
await_result=False,
)
@@ -696,7 +684,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://example.com",
+ "/_matrix/client/v1/media/preview_url?url=http://example.com",
shorthand=False,
)
self.assertEqual(channel.code, 502)
@@ -718,7 +706,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://example.com",
+ "/_matrix/client/v1/media/preview_url?url=http://example.com",
shorthand=False,
)
@@ -741,7 +729,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://example.com",
+ "/_matrix/client/v1/media/preview_url?url=http://example.com",
shorthand=False,
)
@@ -760,7 +748,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
"""
channel = self.make_request(
"OPTIONS",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://example.com",
+ "/_matrix/client/v1/media/preview_url?url=http://example.com",
shorthand=False,
)
self.assertEqual(channel.code, 204)
@@ -774,7 +762,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
# Build and make a request to the server
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://example.com",
+ "/_matrix/client/v1/media/preview_url?url=http://example.com",
shorthand=False,
await_result=False,
)
@@ -827,7 +815,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
await_result=False,
)
@@ -877,7 +865,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
await_result=False,
)
@@ -919,7 +907,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
await_result=False,
)
@@ -959,7 +947,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
await_result=False,
)
@@ -1000,7 +988,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- f"/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?{query_params}",
+ f"/_matrix/client/v1/media/preview_url?{query_params}",
shorthand=False,
)
self.pump()
@@ -1021,7 +1009,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://matrix.org",
+ "/_matrix/client/v1/media/preview_url?url=http://matrix.org",
shorthand=False,
await_result=False,
)
@@ -1058,7 +1046,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://twitter.com/matrixdotorg/status/12345",
+ "/_matrix/client/v1/media/preview_url?url=http://twitter.com/matrixdotorg/status/12345",
shorthand=False,
await_result=False,
)
@@ -1118,7 +1106,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://twitter.com/matrixdotorg/status/12345",
+ "/_matrix/client/v1/media/preview_url?url=http://twitter.com/matrixdotorg/status/12345",
shorthand=False,
await_result=False,
)
@@ -1167,7 +1155,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://www.hulu.com/watch/12345",
+ "/_matrix/client/v1/media/preview_url?url=http://www.hulu.com/watch/12345",
shorthand=False,
await_result=False,
)
@@ -1212,7 +1200,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://twitter.com/matrixdotorg/status/12345",
+ "/_matrix/client/v1/media/preview_url?url=http://twitter.com/matrixdotorg/status/12345",
shorthand=False,
await_result=False,
)
@@ -1241,7 +1229,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://www.twitter.com/matrixdotorg/status/12345",
+ "/_matrix/client/v1/media/preview_url?url=http://www.twitter.com/matrixdotorg/status/12345",
shorthand=False,
await_result=False,
)
@@ -1333,7 +1321,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://www.twitter.com/matrixdotorg/status/12345",
+ "/_matrix/client/v1/media/preview_url?url=http://www.twitter.com/matrixdotorg/status/12345",
shorthand=False,
await_result=False,
)
@@ -1374,7 +1362,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url=http://cdn.twitter.com/matrixdotorg",
+ "/_matrix/client/v1/media/preview_url?url=http://cdn.twitter.com/matrixdotorg",
shorthand=False,
await_result=False,
)
@@ -1416,7 +1404,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
# Check fetching
channel = self.make_request(
"GET",
- f"/_matrix/media/v3/download/{host}/{media_id}",
+ f"/_matrix/client/v1/media/download/{host}/{media_id}",
shorthand=False,
await_result=False,
)
@@ -1429,7 +1417,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- f"/_matrix/media/v3/download/{host}/{media_id}",
+ f"/_matrix/client/v1/download/{host}/{media_id}",
shorthand=False,
await_result=False,
)
@@ -1464,7 +1452,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
# Check fetching
channel = self.make_request(
"GET",
- f"/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/{host}/{media_id}?width=32&height=32&method=scale",
+ f"/_matrix/client/v1/media/thumbnail/{host}/{media_id}?width=32&height=32&method=scale",
shorthand=False,
await_result=False,
)
@@ -1482,7 +1470,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- f"/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/{host}/{media_id}?width=32&height=32&method=scale",
+ f"/_matrix/client/v1/media/thumbnail/{host}/{media_id}?width=32&height=32&method=scale",
shorthand=False,
await_result=False,
)
@@ -1532,8 +1520,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url="
- + bad_url,
+ "/_matrix/client/v1/media/preview_url?url=" + bad_url,
shorthand=False,
await_result=False,
)
@@ -1542,8 +1529,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url="
- + good_url,
+ "/_matrix/client/v1/media/preview_url?url=" + good_url,
shorthand=False,
await_result=False,
)
@@ -1575,8 +1561,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/preview_url?url="
- + bad_url,
+ "/_matrix/client/v1/media/preview_url?url=" + bad_url,
shorthand=False,
await_result=False,
)
@@ -1584,7 +1569,7 @@ class UnstableURLPreviewTests(unittest.HomeserverTestCase):
self.assertEqual(channel.code, 403, channel.result)
-class UnstableMediaConfigTest(unittest.HomeserverTestCase):
+class MediaConfigTest(unittest.HomeserverTestCase):
servlets = [
media.register_servlets,
admin.register_servlets,
@@ -1595,7 +1580,6 @@ class UnstableMediaConfigTest(unittest.HomeserverTestCase):
self, reactor: ThreadedMemoryReactorClock, clock: Clock
) -> HomeServer:
config = self.default_config()
- config["experimental_features"] = {"msc3916_authenticated_media_enabled": True}
self.storage_path = self.mktemp()
self.media_store_path = self.mktemp()
@@ -1622,7 +1606,7 @@ class UnstableMediaConfigTest(unittest.HomeserverTestCase):
def test_media_config(self) -> None:
channel = self.make_request(
"GET",
- "/_matrix/client/unstable/org.matrix.msc3916/media/config",
+ "/_matrix/client/v1/media/config",
shorthand=False,
access_token=self.tok,
)
@@ -1899,7 +1883,7 @@ input_values = [(x,) for x in test_images]
@parameterized_class(("test_image",), input_values)
-class DownloadTestCase(unittest.HomeserverTestCase):
+class DownloadAndThumbnailTestCase(unittest.HomeserverTestCase):
test_image: ClassVar[TestImage]
servlets = [
media.register_servlets,
@@ -2005,7 +1989,6 @@ class DownloadTestCase(unittest.HomeserverTestCase):
"config": {"directory": self.storage_path},
}
config["media_storage_providers"] = [provider_config]
- config["experimental_features"] = {"msc3916_authenticated_media_enabled": True}
hs = self.setup_test_homeserver(config=config, federation_http_client=client)
@@ -2164,7 +2147,7 @@ class DownloadTestCase(unittest.HomeserverTestCase):
def test_unknown_federation_endpoint(self) -> None:
"""
- Test that if the downloadd request to remote federation endpoint returns a 404
+ Test that if the download request to remote federation endpoint returns a 404
we fall back to the _matrix/media endpoint
"""
channel = self.make_request(
@@ -2210,3 +2193,236 @@ class DownloadTestCase(unittest.HomeserverTestCase):
self.pump()
self.assertEqual(channel.code, 200)
+
+ def test_thumbnail_crop(self) -> None:
+ """Test that a cropped remote thumbnail is available."""
+ self._test_thumbnail(
+ "crop",
+ self.test_image.expected_cropped,
+ expected_found=self.test_image.expected_found,
+ unable_to_thumbnail=self.test_image.unable_to_thumbnail,
+ )
+
+ def test_thumbnail_scale(self) -> None:
+ """Test that a scaled remote thumbnail is available."""
+ self._test_thumbnail(
+ "scale",
+ self.test_image.expected_scaled,
+ expected_found=self.test_image.expected_found,
+ unable_to_thumbnail=self.test_image.unable_to_thumbnail,
+ )
+
+ def test_invalid_type(self) -> None:
+ """An invalid thumbnail type is never available."""
+ self._test_thumbnail(
+ "invalid",
+ None,
+ expected_found=False,
+ unable_to_thumbnail=self.test_image.unable_to_thumbnail,
+ )
+
+ @unittest.override_config(
+ {"thumbnail_sizes": [{"width": 32, "height": 32, "method": "scale"}]}
+ )
+ def test_no_thumbnail_crop(self) -> None:
+ """
+ Override the config to generate only scaled thumbnails, but request a cropped one.
+ """
+ self._test_thumbnail(
+ "crop",
+ None,
+ expected_found=False,
+ unable_to_thumbnail=self.test_image.unable_to_thumbnail,
+ )
+
+ @unittest.override_config(
+ {"thumbnail_sizes": [{"width": 32, "height": 32, "method": "crop"}]}
+ )
+ def test_no_thumbnail_scale(self) -> None:
+ """
+ Override the config to generate only cropped thumbnails, but request a scaled one.
+ """
+ self._test_thumbnail(
+ "scale",
+ None,
+ expected_found=False,
+ unable_to_thumbnail=self.test_image.unable_to_thumbnail,
+ )
+
+ def test_thumbnail_repeated_thumbnail(self) -> None:
+ """Test that fetching the same thumbnail works, and deleting the on disk
+ thumbnail regenerates it.
+ """
+ self._test_thumbnail(
+ "scale",
+ self.test_image.expected_scaled,
+ expected_found=self.test_image.expected_found,
+ unable_to_thumbnail=self.test_image.unable_to_thumbnail,
+ )
+
+ if not self.test_image.expected_found:
+ return
+
+ # Fetching again should work, without re-requesting the image from the
+ # remote.
+ params = "?width=32&height=32&method=scale"
+ channel = self.make_request(
+ "GET",
+ f"/_matrix/client/v1/media/thumbnail/{self.remote}/{self.media_id}{params}",
+ shorthand=False,
+ await_result=False,
+ access_token=self.tok,
+ )
+ self.pump()
+
+ self.assertEqual(channel.code, 200)
+ if self.test_image.expected_scaled:
+ self.assertEqual(
+ channel.result["body"],
+ self.test_image.expected_scaled,
+ channel.result["body"],
+ )
+
+ # Deleting the thumbnail on disk then re-requesting it should work as
+ # Synapse should regenerate missing thumbnails.
+ info = self.get_success(
+ self.store.get_cached_remote_media(self.remote, self.media_id)
+ )
+ assert info is not None
+ file_id = info.filesystem_id
+
+ thumbnail_dir = self.media_repo.filepaths.remote_media_thumbnail_dir(
+ self.remote, file_id
+ )
+ shutil.rmtree(thumbnail_dir, ignore_errors=True)
+
+ channel = self.make_request(
+ "GET",
+ f"/_matrix/client/v1/media/thumbnail/{self.remote}/{self.media_id}{params}",
+ shorthand=False,
+ await_result=False,
+ access_token=self.tok,
+ )
+ self.pump()
+
+ self.assertEqual(channel.code, 200)
+ if self.test_image.expected_scaled:
+ self.assertEqual(
+ channel.result["body"],
+ self.test_image.expected_scaled,
+ channel.result["body"],
+ )
+
+ def _test_thumbnail(
+ self,
+ method: str,
+ expected_body: Optional[bytes],
+ expected_found: bool,
+ unable_to_thumbnail: bool = False,
+ ) -> None:
+ """Test the given thumbnailing method works as expected.
+
+ Args:
+ method: The thumbnailing method to use (crop, scale).
+ expected_body: The expected bytes from thumbnailing, or None if
+ test should just check for a valid image.
+ expected_found: True if the file should exist on the server, or False if
+ a 404/400 is expected.
+ unable_to_thumbnail: True if we expect the thumbnailing to fail (400), or
+ False if the thumbnailing should succeed or a normal 404 is expected.
+ """
+
+ params = "?width=32&height=32&method=" + method
+ channel = self.make_request(
+ "GET",
+ f"/_matrix/client/v1/media/thumbnail/{self.remote}/{self.media_id}{params}",
+ shorthand=False,
+ await_result=False,
+ access_token=self.tok,
+ )
+ self.pump()
+ headers = {
+ b"Content-Length": [b"%d" % (len(self.test_image.data))],
+ b"Content-Type": [self.test_image.content_type],
+ }
+ self.fetches[0][0].callback(
+ (self.test_image.data, (len(self.test_image.data), headers))
+ )
+ self.pump()
+ if expected_found:
+ self.assertEqual(channel.code, 200)
+
+ self.assertEqual(
+ channel.headers.getRawHeaders(b"Cross-Origin-Resource-Policy"),
+ [b"cross-origin"],
+ )
+
+ if expected_body is not None:
+ self.assertEqual(
+ channel.result["body"], expected_body, channel.result["body"]
+ )
+ else:
+ # ensure that the result is at least some valid image
+ Image.open(io.BytesIO(channel.result["body"]))
+ elif unable_to_thumbnail:
+ # A 400 with a JSON body.
+ self.assertEqual(channel.code, 400)
+ self.assertEqual(
+ channel.json_body,
+ {
+ "errcode": "M_UNKNOWN",
+ "error": "Cannot find any thumbnails for the requested media ('/_matrix/client/v1/media/thumbnail/example.com/12345'). This might mean the media is not a supported_media_format=(image/jpeg, image/jpg, image/webp, image/gif, image/png) or that thumbnailing failed for some other reason. (Dynamic thumbnails are disabled on this server.)",
+ },
+ )
+ else:
+ # A 404 with a JSON body.
+ self.assertEqual(channel.code, 404)
+ self.assertEqual(
+ channel.json_body,
+ {
+ "errcode": "M_NOT_FOUND",
+ "error": "Not found '/_matrix/client/v1/media/thumbnail/example.com/12345'",
+ },
+ )
+
+ @parameterized.expand([("crop", 16), ("crop", 64), ("scale", 16), ("scale", 64)])
+ def test_same_quality(self, method: str, desired_size: int) -> None:
+ """Test that choosing between thumbnails with the same quality rating succeeds.
+
+ We are not particular about which thumbnail is chosen."""
+
+ content_type = self.test_image.content_type.decode()
+ media_repo = self.hs.get_media_repository()
+ thumbnail_provider = ThumbnailProvider(
+ self.hs, media_repo, media_repo.media_storage
+ )
+
+ self.assertIsNotNone(
+ thumbnail_provider._select_thumbnail(
+ desired_width=desired_size,
+ desired_height=desired_size,
+ desired_method=method,
+ desired_type=content_type,
+ # Provide two identical thumbnails which are guaranteed to have the same
+ # quality rating.
+ thumbnail_infos=[
+ ThumbnailInfo(
+ width=32,
+ height=32,
+ method=method,
+ type=content_type,
+ length=256,
+ ),
+ ThumbnailInfo(
+ width=32,
+ height=32,
+ method=method,
+ type=content_type,
+ length=256,
+ ),
+ ],
+ file_id=f"image{self.test_image.extension.decode()}",
+ url_cache=False,
+ server_name=None,
+ )
+ )
|