Repository: bookwyrm-social/bookwyrm

Issue:
External Storage: Image preview in status update has wrong URL
Following this message:

https://social.apreslanu.it/users/tk/statuses/106693318429554036

The image is unavailable, because its URL is `https://lire.boitam.euhttps//lire-boitam-eu.s3.fr-par.scw.cloud/images/covers/15c8e1f6-4df9-4045-ae6a-8248226a1791.jpeg`

It looks like `https://DOMAIN` has been added to the full image URL.

I think it might originate in this code, though I don't know what went wrong:

https://github.com/bookwyrm-social/bookwyrm/blob/6d82bafdc01d9bad856b41129a78b4fa92a15755/bookwyrm/models/fields.py#L352-L360


Discussion:
Or was it fixed in #1242? I might be a few commits behind on my instance.

Resolving patch:
diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py
--- a/bookwyrm/models/fields.py
+++ b/bookwyrm/models/fields.py
@@ -3,6 +3,7 @@
 import imghdr
 import re
 from uuid import uuid4
+from urllib.parse import urljoin
 
 import dateutil.parser
 from dateutil.parser import ParserError
@@ -13,11 +14,12 @@
 from django.forms import ClearableFileInput, ImageField as DjangoImageField
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
+from django.utils.encoding import filepath_to_uri
 
 from bookwyrm import activitypub
 from bookwyrm.connectors import get_image
 from bookwyrm.sanitize_html import InputHtmlParser
-from bookwyrm.settings import DOMAIN
+from bookwyrm.settings import MEDIA_FULL_URL
 
 
 def validate_remote_id(value):
@@ -387,8 +389,10 @@ def image_serializer(value, alt):
         url = value.url
     else:
         return None
-    if not url[:4] == "http":
-        url = f"https://{DOMAIN}{url}"
+    if url is not None:
+        url = url.lstrip("/")
+    url = urljoin(MEDIA_FULL_URL, url)
+
     return activitypub.Document(url=url, name=alt)
 
 
@@ -424,7 +428,12 @@ def set_activity_from_field(self, activity, instance):
         activity[key] = formatted
 
     def field_to_activity(self, value, alt=None):
-        return image_serializer(value, alt)
+        url = self.get_absolute_url(value)
+
+        if not url:
+            return None
+
+        return activitypub.Document(url=url, name=alt)
 
     def field_from_activity(self, value):
         image_slug = value
@@ -460,6 +469,20 @@ def formfield(self, **kwargs):
             }
         )
 
+    # pylint: disable=no-self-use
+    def get_absolute_url(self, value):
+        """returns an absolute URL for the image"""
+        name = getattr(value, "name")
+        if not name:
+            return None
+
+        url = filepath_to_uri(name)
+        if url is not None:
+            url = url.lstrip("/")
+        url = urljoin(MEDIA_FULL_URL, url)
+
+        return url
+
 
 class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
     """activitypub-aware datetime field"""


Test patch:
diff --git a/bookwyrm/tests/models/test_fields.py b/bookwyrm/tests/models/test_fields.py
--- a/bookwyrm/tests/models/test_fields.py
+++ b/bookwyrm/tests/models/test_fields.py
@@ -22,6 +22,7 @@
 from bookwyrm.models import fields, User, Status
 from bookwyrm.models.base_model import BookWyrmModel
 from bookwyrm.models.activitypub_mixin import ActivitypubMixin
+from bookwyrm.settings import DOMAIN
 
 # pylint: disable=too-many-public-methods
 @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@@ -424,21 +425,18 @@ def test_image_field(self, *_):
         image.save(output, format=image.format)
         user.avatar.save("test.jpg", ContentFile(output.getvalue()))
 
-        output = fields.image_serializer(user.avatar, alt="alt text")
+        instance = fields.ImageField()
+
+        output = instance.field_to_activity(user.avatar)
         self.assertIsNotNone(
             re.match(
-                r".*\.jpg",
+                fr"https:\/\/{DOMAIN}\/.*\.jpg",
                 output.url,
             )
         )
-        self.assertEqual(output.name, "alt text")
+        self.assertEqual(output.name, "")
         self.assertEqual(output.type, "Document")
 
-        instance = fields.ImageField()
-
-        output = fields.image_serializer(user.avatar, alt=None)
-        self.assertEqual(instance.field_to_activity(user.avatar), output)
-
         responses.add(
             responses.GET,
             "http://www.example.com/image.jpg",
@@ -452,7 +450,7 @@ def test_image_field(self, *_):
     def test_image_serialize(self, *_):
         """make sure we're creating sensible image paths"""
         ValueMock = namedtuple("ValueMock", ("url"))
-        value_mock = ValueMock("/images/fish.jpg")
+        value_mock = ValueMock("https://your.domain.here/images/fish.jpg")
         result = fields.image_serializer(value_mock, "hello")
         self.assertEqual(result.type, "Document")
         self.assertEqual(result.url, "https://your.domain.here/images/fish.jpg")
diff --git a/bookwyrm/tests/models/test_status_model.py b/bookwyrm/tests/models/test_status_model.py
--- a/bookwyrm/tests/models/test_status_model.py
+++ b/bookwyrm/tests/models/test_status_model.py
@@ -2,6 +2,7 @@
 from unittest.mock import patch
 from io import BytesIO
 import pathlib
+from urllib.parse import urljoin
 
 from django.http import Http404
 from django.core.files.base import ContentFile
@@ -192,7 +193,7 @@ def test_generated_note_to_pure_activity(self, *_):
         self.assertEqual(activity["attachment"][0].type, "Document")
         self.assertEqual(
             activity["attachment"][0].url,
-            f"https://{settings.DOMAIN}{self.book.cover.url}",
+            urljoin(settings.MEDIA_FULL_URL, self.book.cover.url.lstrip("/")),
         )
         self.assertEqual(activity["attachment"][0].name, "Test Edition")
 
@@ -222,7 +223,7 @@ def test_comment_to_pure_activity(self, *_):
         self.assertEqual(activity["attachment"][0].type, "Document")
         self.assertEqual(
             activity["attachment"][0].url,
-            f"https://{settings.DOMAIN}{self.book.cover.url}",
+            urljoin(settings.MEDIA_FULL_URL, self.book.cover.url.lstrip("/")),
         )
         self.assertEqual(activity["attachment"][0].name, "Test Edition")
 
@@ -259,7 +260,7 @@ def test_quotation_to_pure_activity(self, *_):
         self.assertEqual(activity["attachment"][0].type, "Document")
         self.assertEqual(
             activity["attachment"][0].url,
-            f"https://{settings.DOMAIN}{self.book.cover.url}",
+            urljoin(settings.MEDIA_FULL_URL, self.book.cover.url.lstrip("/")),
         )
         self.assertEqual(activity["attachment"][0].name, "Test Edition")
 
@@ -300,7 +301,7 @@ def test_review_to_pure_activity(self, *_):
         self.assertEqual(activity["attachment"][0].type, "Document")
         self.assertEqual(
             activity["attachment"][0].url,
-            f"https://{settings.DOMAIN}{self.book.cover.url}",
+            urljoin(settings.MEDIA_FULL_URL, self.book.cover.url.lstrip("/")),
         )
         self.assertEqual(activity["attachment"][0].name, "Test Edition")
 
@@ -322,7 +323,7 @@ def test_review_to_pure_activity_no_rating(self, *_):
         self.assertEqual(activity["attachment"][0].type, "Document")
         self.assertEqual(
             activity["attachment"][0].url,
-            f"https://{settings.DOMAIN}{self.book.cover.url}",
+            urljoin(settings.MEDIA_FULL_URL, self.book.cover.url.lstrip("/")),
         )
         self.assertEqual(activity["attachment"][0].name, "Test Edition")
 
@@ -343,7 +344,7 @@ def test_reviewrating_to_pure_activity(self, *_):
         self.assertEqual(activity["attachment"][0].type, "Document")
         self.assertEqual(
             activity["attachment"][0].url,
-            f"https://{settings.DOMAIN}{self.book.cover.url}",
+            urljoin(settings.MEDIA_FULL_URL, self.book.cover.url.lstrip("/")),
         )
         self.assertEqual(activity["attachment"][0].name, "Test Edition")