diff --git a/federation/entities/activitypub/entities.py b/federation/entities/activitypub/entities.py index f355ebf..00cf521 100644 --- a/federation/entities/activitypub/entities.py +++ b/federation/entities/activitypub/entities.py @@ -2,11 +2,14 @@ import logging import uuid from typing import Dict +import attr + from federation.entities.activitypub.constants import ( CONTEXTS_DEFAULT, CONTEXT_MANUALLY_APPROVES_FOLLOWERS, CONTEXT_SENSITIVE, CONTEXT_HASHTAG, CONTEXT_LD_SIGNATURES) from federation.entities.activitypub.enums import ActorType, ObjectType, ActivityType from federation.entities.activitypub.mixins import ActivitypubObjectMixin, ActivitypubActorMixin +from federation.entities.activitypub.objects import ImageObject from federation.entities.base import Profile, Post, Follow, Accept from federation.outbound import handle_send from federation.types import UserType @@ -168,5 +171,8 @@ class ActivitypubProfile(ActivitypubActorMixin, Profile): if self.raw_content: as2['summary'] = self.raw_content if self.image_urls.get('large'): - as2['icon'] = self.image_urls.get('large') + try: + as2['icon'] = attr.asdict(ImageObject(url=self.image_urls.get('large'))) + except Exception as ex: + logger.warning("ActivitypubProfile.to_as2 - failed to set profile icon: %s", ex) return as2 diff --git a/federation/entities/activitypub/objects.py b/federation/entities/activitypub/objects.py new file mode 100644 index 0000000..6745001 --- /dev/null +++ b/federation/entities/activitypub/objects.py @@ -0,0 +1,25 @@ +import attr + +from federation.utils.network import fetch_content_type + + +@attr.s +class ImageObject: + """ + An Image object for AS2 serialization. + """ + _allowed_types = ( + "image/jpeg", + "image/png", + "image/gif", + ) + url: str = attr.ib() + type: str = attr.ib(default="Image") + mediaType: str = attr.ib() + + @mediaType.default + def cache_media_type(self): + content_type = fetch_content_type(self.url) + if content_type in self._allowed_types: + return content_type + return "" diff --git a/federation/tests/entities/activitypub/test_entities.py b/federation/tests/entities/activitypub/test_entities.py index e264aef..373ac25 100644 --- a/federation/tests/entities/activitypub/test_entities.py +++ b/federation/tests/entities/activitypub/test_entities.py @@ -79,7 +79,8 @@ class TestEntitiesConvertToAS2: 'published': '2019-04-27T00:00:00', } - def test_profile_to_as2(self, activitypubprofile): + @patch("federation.entities.activitypub.objects.fetch_content_type", return_value="image/jpeg") + def test_profile_to_as2(self, mock_fetch, activitypubprofile): result = activitypubprofile.to_as2() assert result == { "@context": CONTEXTS_DEFAULT + [ @@ -104,7 +105,11 @@ class TestEntitiesConvertToAS2: "type": "Person", "url": "https://example.com/bob-bobertson", "summary": "foobar", - "icon": "urllarge", + "icon": { + "type": "Image", + "url": "urllarge", + "mediaType": "image/jpeg", + } } diff --git a/federation/utils/network.py b/federation/utils/network.py index 865a9a1..3edb7cb 100644 --- a/federation/utils/network.py +++ b/federation/utils/network.py @@ -3,7 +3,7 @@ import datetime import logging import re import socket -from typing import Tuple +from typing import Optional, Tuple import requests from ipdata import ipdata @@ -18,6 +18,18 @@ logger = logging.getLogger("federation") USER_AGENT = "python/federation/%s" % __version__ +def fetch_content_type(url: str) -> Optional[str]: + """ + Fetch the HEAD of the remote url to determine the content type. + """ + try: + response = requests.head(url, headers={'user-agent': USER_AGENT}, timeout=10) + except RequestException as ex: + logger.warning("fetch_content_type - %s when fetching url %s", ex, url) + else: + return response.headers.get('Content-Type') + + def fetch_country_by_ip(ip): """ Fetches country code by IP