From 759d5ac0529c1d8935b3b74bcdf42fb9e55b102f Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Sat, 22 Jul 2023 10:38:22 -0600 Subject: [PATCH] Fixed #616: Do followers-only properly --- activities/models/post.py | 8 +++++- docs/releases/0.10.rst | 9 ++++-- tests/activities/models/test_post.py | 42 ++++++++++++++++++++++++++++ users/models/identity.py | 26 +++++++++++++---- 4 files changed, 77 insertions(+), 8 deletions(-) diff --git a/activities/models/post.py b/activities/models/post.py index f764d07..6ae3349 100644 --- a/activities/models/post.py +++ b/activities/models/post.py @@ -623,6 +623,7 @@ class Post(StatorModel): """ Returns the AP JSON for this object """ + self.author.ensure_uris() value = { "to": [], "cc": [], @@ -655,11 +656,14 @@ class Post(StatorModel): if self.edited: value["updated"] = format_ld_date(self.edited) # Targeting - # TODO: Add followers object if self.visibility == self.Visibilities.public: value["to"].append("as:Public") elif self.visibility == self.Visibilities.unlisted: value["cc"].append("as:Public") + elif ( + self.visibility == self.Visibilities.followers and self.author.followers_uri + ): + value["to"].append(self.author.followers_uri) # Mentions for mention in self.mentions.all(): value["tag"].append(mention.to_ap_tag()) @@ -922,6 +926,8 @@ class Post(StatorModel): post.visibility = Post.Visibilities.public elif "public" in cc or "as:public" in cc: post.visibility = Post.Visibilities.unlisted + elif post.author.followers_uri in to: + post.visibility = Post.Visibilities.followers # Attachments # These have no IDs, so we have to wipe them each time post.attachments.all().delete() diff --git a/docs/releases/0.10.rst b/docs/releases/0.10.rst index 4580002..36ce334 100644 --- a/docs/releases/0.10.rst +++ b/docs/releases/0.10.rst @@ -1,5 +1,5 @@ -0.9 -=== +0.10 +==== *Released: Not Yet Released* @@ -15,6 +15,11 @@ This release's major changes: * TBC +Minor changes also include: + +* Followers-only mode now works correctly inbound and outbound (though outbound + may need the other server to refresh the profile first). + If you'd like to help with code, design, or other areas, see :doc:`/contributing` to see how to get in touch. diff --git a/tests/activities/models/test_post.py b/tests/activities/models/test_post.py index 9b094a4..96125da 100644 --- a/tests/activities/models/test_post.py +++ b/tests/activities/models/test_post.py @@ -499,3 +499,45 @@ def test_post_hashtag_to_ap(identity: Identity, config_system): ] assert "#world" in ap["object"]["content"] assert 'rel="tag"' in ap["object"]["content"] + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "visibility", + [ + Post.Visibilities.public, + Post.Visibilities.unlisted, + Post.Visibilities.followers, + Post.Visibilities.mentioned, + ], +) +def test_post_targets_to_ap( + identity: Identity, other_identity: Identity, visibility: Post.Visibilities +): + """ + Ensures that posts have the right targets in AP form. + """ + + # Make a post + post = Post.objects.create( + content="

Hello @other

", + author=identity, + local=True, + visibility=visibility, + ) + post.mentions.add(other_identity) + + # Check its AP targets + ap_dict = post.to_ap() + if visibility == Post.Visibilities.public: + assert ap_dict["to"] == ["as:Public"] + assert ap_dict["cc"] == [other_identity.actor_uri] + elif visibility == Post.Visibilities.unlisted: + assert "to" not in ap_dict + assert ap_dict["cc"] == ["as:Public", other_identity.actor_uri] + elif visibility == Post.Visibilities.followers: + assert ap_dict["to"] == [identity.followers_uri] + assert ap_dict["cc"] == [other_identity.actor_uri] + elif visibility == Post.Visibilities.mentioned: + assert "to" not in ap_dict + assert ap_dict["cc"] == [other_identity.actor_uri] diff --git a/users/models/identity.py b/users/models/identity.py index aa1b34b..32d30a5 100644 --- a/users/models/identity.py +++ b/users/models/identity.py @@ -317,6 +317,19 @@ class Identity(StatorModel): for data in self.metadata ] + def ensure_uris(self): + """ + Ensures that local identities have all the URIs populated on their fields + (this lets us add new ones easily) + """ + if self.local: + self.inbox_uri = self.actor_uri + "inbox/" + self.outbox_uri = self.actor_uri + "outbox/" + self.featured_collection_uri = self.actor_uri + "collections/featured/" + self.followers_uri = self.actor_uri + "followers/" + self.following_uri = self.actor_uri + "following/" + self.shared_inbox_uri = f"https://{self.domain.uri_domain}/inbox/" + ### Alternate constructors/fetchers ### @classmethod @@ -482,12 +495,15 @@ class Identity(StatorModel): def to_ap(self): from activities.models import Emoji + self.ensure_uris() response = { "id": self.actor_uri, "type": self.actor_type.title(), - "inbox": self.actor_uri + "inbox/", - "outbox": self.actor_uri + "outbox/", - "featured": self.actor_uri + "collections/featured/", + "inbox": self.inbox_uri, + "outbox": self.outbox_uri, + "featured": self.featured_collection_uri, + "followers": self.followers_uri, + "following": self.following_uri, "preferredUsername": self.username, "publicKey": { "id": self.public_key_id, @@ -514,9 +530,9 @@ class Identity(StatorModel): "mediaType": media_type_from_filename(self.image.name), "url": self.image.url, } - if self.local: + if self.shared_inbox_uri: response["endpoints"] = { - "sharedInbox": f"https://{self.domain.uri_domain}/inbox/", + "sharedInbox": self.shared_inbox_uri, } if self.metadata: response["attachment"] = [