summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Manfre2022-11-26 12:09:31 -0500
committerGitHub2022-11-26 10:09:31 -0700
commit849c221aeeee89bdb61a88b4e6080481ecfeb934 (patch)
tree323b5a720adf214f4bbbbdaadbcd30249bcdcec5
parentc7588583927e004e10599912f6d7b76413d52730 (diff)
downloadtakahe-849c221aeeee89bdb61a88b4e6080481ecfeb934.tar.gz
takahe-849c221aeeee89bdb61a88b4e6080481ecfeb934.tar.bz2
takahe-849c221aeeee89bdb61a88b4e6080481ecfeb934.zip
Local-only posting
-rw-r--r--activities/migrations/0001_initial.py1
-rw-r--r--activities/models/post.py11
-rw-r--r--activities/views/posts.py3
-rw-r--r--templates/activities/_post.html2
-rw-r--r--tests/activities/models/test_post_targets.py107
-rw-r--r--tests/conftest.py27
6 files changed, 144 insertions, 7 deletions
diff --git a/activities/migrations/0001_initial.py b/activities/migrations/0001_initial.py
index 82a9085..f7961df 100644
--- a/activities/migrations/0001_initial.py
+++ b/activities/migrations/0001_initial.py
@@ -60,6 +60,7 @@ class Migration(migrations.Migration):
models.IntegerField(
choices=[
(0, "Public"),
+ (4, "Local Only"),
(1, "Unlisted"),
(2, "Followers"),
(3, "Mentioned"),
diff --git a/activities/models/post.py b/activities/models/post.py
index dd82bcf..f75c526 100644
--- a/activities/models/post.py
+++ b/activities/models/post.py
@@ -64,6 +64,7 @@ class Post(StatorModel):
class Visibilities(models.IntegerChoices):
public = 0
+ local_only = 4
unlisted = 1
followers = 2
mentioned = 3
@@ -261,6 +262,9 @@ class Post(StatorModel):
mentions.add(identity)
if reply_to:
mentions.add(reply_to.author)
+ # Maintain local-only for replies
+ if reply_to.visibility == reply_to.Visibilities.local_only:
+ visibility = reply_to.Visibilities.local_only
# Strip all HTML and apply linebreaks filter
content = linebreaks_filter(strip_html(content))
# Make the Post object
@@ -361,11 +365,12 @@ class Post(StatorModel):
reply_post = await self.ain_reply_to_post()
if reply_post:
targets.add(reply_post.author)
- # If this is a remote post, filter to only include local identities
- if not self.local:
+ # If this is a remote post or local-only, filter to only include
+ # local identities
+ if not self.local or self.visibility == Post.Visibilities.local_only:
targets = {target for target in targets if target.local}
# If it's a local post, include the author
- else:
+ if self.local:
targets.add(self.author)
return targets
diff --git a/activities/views/posts.py b/activities/views/posts.py
index 8cd91a1..e1609cc 100644
--- a/activities/views/posts.py
+++ b/activities/views/posts.py
@@ -172,6 +172,7 @@ class Compose(FormView):
visibility = forms.ChoiceField(
choices=[
(Post.Visibilities.public, "Public"),
+ (Post.Visibilities.local_only, "Local Only"),
(Post.Visibilities.unlisted, "Unlisted"),
(Post.Visibilities.followers, "Followers & Mentioned Only"),
(Post.Visibilities.mentioned, "Mentioned Only"),
@@ -207,7 +208,7 @@ class Compose(FormView):
] = self.request.identity.config_identity.default_post_visibility
if self.reply_to:
initial["reply_to"] = self.reply_to.pk
- initial["visibility"] = Post.Visibilities.unlisted
+ initial["visibility"] = self.reply_to.visibility
initial["text"] = f"@{self.reply_to.author.handle} "
return initial
diff --git a/templates/activities/_post.html b/templates/activities/_post.html
index 06aa3f7..ebe5696 100644
--- a/templates/activities/_post.html
+++ b/templates/activities/_post.html
@@ -15,6 +15,8 @@
<i class="visibility fa-solid fa-lock" title="Followers Only"></i>
{% elif post.visibility == 3 %}
<i class="visibility fa-solid fa-at" title="Mentioned Only"></i>
+ {% elif post.visibility == 4 %}
+ <i class="visibility fa-solid fa-link-slash" title="Local Only"></i>
{% endif %}
<a href="{{ post.url }}">
{% if post.published %}
diff --git a/tests/activities/models/test_post_targets.py b/tests/activities/models/test_post_targets.py
new file mode 100644
index 0000000..1e4bbc4
--- /dev/null
+++ b/tests/activities/models/test_post_targets.py
@@ -0,0 +1,107 @@
+import pytest
+from asgiref.sync import async_to_sync
+
+from activities.models import Post
+from users.models import Follow
+
+
+@pytest.mark.django_db
+def test_post_targets_simple(identity, other_identity, remote_identity):
+ """
+ Tests that a simple top level post returns the correct targets.
+ """
+ # Test a post with no mentions targets author
+ post = Post.objects.create(
+ content="<p>Hello</p>",
+ author=identity,
+ local=True,
+ )
+ targets = async_to_sync(post.aget_targets)()
+ assert targets == {identity}
+
+ # Test remote reply targets original post author
+ Post.objects.create(
+ content="<p>Reply</p>",
+ author=remote_identity,
+ local=False,
+ in_reply_to=post.absolute_object_uri(),
+ )
+ targets = async_to_sync(post.aget_targets)()
+ assert targets == {identity}
+
+ # Test a post with local and remote mentions
+ post = Post.objects.create(
+ content="<p>Hello @test and @other</p>",
+ author=identity,
+ local=True,
+ )
+ # Mentions are targeted
+ post.mentions.add(remote_identity)
+ post.mentions.add(other_identity)
+ targets = async_to_sync(post.aget_targets)()
+ # Targets everyone
+ assert targets == {identity, other_identity, remote_identity}
+
+ # Test remote post with mentions
+ post.local = False
+ post.save()
+ targets = async_to_sync(post.aget_targets)()
+ # Only targets locals
+ assert targets == {identity, other_identity}
+
+
+@pytest.mark.django_db
+def test_post_local_only(identity, other_identity, remote_identity):
+ """
+ Tests that a simple top level post returns the correct targets.
+ """
+ # Test a short username (remote)
+ post = Post.objects.create(
+ content="<p>Hello @test and @other</p>",
+ author=identity,
+ local=True,
+ visibility=Post.Visibilities.local_only,
+ )
+ post.mentions.add(remote_identity)
+ post.mentions.add(other_identity)
+
+ # Remote mention is not targeted
+ post.mentions.add(remote_identity)
+ targets = async_to_sync(post.aget_targets)()
+ assert targets == {identity, other_identity}
+
+
+@pytest.mark.django_db
+def test_post_followers(identity, other_identity, remote_identity):
+
+ Follow.objects.create(source=other_identity, target=identity)
+ Follow.objects.create(source=remote_identity, target=identity)
+
+ # Test Public post w/o mentions targets self and followers
+ post = Post.objects.create(
+ content="<p>Hello</p>",
+ author=identity,
+ local=True,
+ visibility=Post.Visibilities.public,
+ )
+ targets = async_to_sync(post.aget_targets)()
+ assert targets == {identity, other_identity, remote_identity}
+
+ # Remote post only targets local followers
+ post.local = False
+ post.save()
+ targets = async_to_sync(post.aget_targets)()
+ assert targets == {identity, other_identity}
+
+ # Local Only post only targets local followers
+ post.local = True
+ post.visibility = Post.Visibilities.local_only
+ post.save()
+ targets = async_to_sync(post.aget_targets)()
+ assert targets == {identity, other_identity}
+
+ # Mentioned posts do not target unmentioned followers
+ post.visibility = Post.Visibilities.mentioned
+ post.save()
+ targets = async_to_sync(post.aget_targets)()
+ assert targets == {identity}
diff --git a/tests/conftest.py b/tests/conftest.py
index 48ee95a..d506c5c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -68,11 +68,16 @@ def user() -> User:
@pytest.fixture
@pytest.mark.django_db
-def identity(user):
+def domain() -> Domain:
+ return Domain.objects.create(domain="example.com", local=True, public=True)
+
+
+@pytest.fixture
+@pytest.mark.django_db
+def identity(user, domain) -> Identity:
"""
Creates a basic test identity with a user and domain.
"""
- domain = Domain.objects.create(domain="example.com", local=True, public=True)
identity = Identity.objects.create(
actor_uri="https://example.com/@test@example.com/",
username="test",
@@ -85,8 +90,24 @@ def identity(user):
@pytest.fixture
+def other_identity(user, domain) -> Identity:
+ """
+ Creates a different basic test identity with a user and domain.
+ """
+ identity = Identity.objects.create(
+ actor_uri="https://example.com/@other@example.com/",
+ username="other",
+ domain=domain,
+ name="Other User",
+ local=True,
+ )
+ identity.users.set([user])
+ return identity
+
+
+@pytest.fixture
@pytest.mark.django_db
-def remote_identity():
+def remote_identity() -> Identity:
"""
Creates a basic remote test identity with a domain.
"""