mirror of
https://mau.dev/maunium/synapse.git
synced 2024-10-01 01:36:05 -04:00
Support MSC3916 by adding a federation /thumbnail endpoint and authenticated _matrix/client/v1/media/thumbnail
endpoint (#17388)
[MSC3916](https://github.com/matrix-org/matrix-spec-proposals/pull/3916) added the endpoints `_matrix/federation/v1/media/thumbnail` and the authenticated `_matrix/client/v1/media/thumbnail`. This PR implements those endpoints, along with stabilizing `_matrix/client/v1/media/config` and `_matrix/client/v1/media/preview_url`. Complement tests are at https://github.com/matrix-org/complement/pull/728
This commit is contained in:
parent
20de685a4b
commit
cf69f8d59b
3
changelog.d/17388.feature
Normal file
3
changelog.d/17388.feature
Normal file
@ -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.
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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'",
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user