Local-only posting

This commit is contained in:
Michael Manfre 2022-11-26 12:09:31 -05:00 committed by GitHub
parent c758858392
commit 849c221aee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 144 additions and 7 deletions

View file

@ -60,6 +60,7 @@ class Migration(migrations.Migration):
models.IntegerField( models.IntegerField(
choices=[ choices=[
(0, "Public"), (0, "Public"),
(4, "Local Only"),
(1, "Unlisted"), (1, "Unlisted"),
(2, "Followers"), (2, "Followers"),
(3, "Mentioned"), (3, "Mentioned"),

View file

@ -64,6 +64,7 @@ class Post(StatorModel):
class Visibilities(models.IntegerChoices): class Visibilities(models.IntegerChoices):
public = 0 public = 0
local_only = 4
unlisted = 1 unlisted = 1
followers = 2 followers = 2
mentioned = 3 mentioned = 3
@ -261,6 +262,9 @@ class Post(StatorModel):
mentions.add(identity) mentions.add(identity)
if reply_to: if reply_to:
mentions.add(reply_to.author) 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 # Strip all HTML and apply linebreaks filter
content = linebreaks_filter(strip_html(content)) content = linebreaks_filter(strip_html(content))
# Make the Post object # Make the Post object
@ -361,11 +365,12 @@ class Post(StatorModel):
reply_post = await self.ain_reply_to_post() reply_post = await self.ain_reply_to_post()
if reply_post: if reply_post:
targets.add(reply_post.author) targets.add(reply_post.author)
# If this is a remote post, filter to only include local identities # If this is a remote post or local-only, filter to only include
if not self.local: # local identities
if not self.local or self.visibility == Post.Visibilities.local_only:
targets = {target for target in targets if target.local} targets = {target for target in targets if target.local}
# If it's a local post, include the author # If it's a local post, include the author
else: if self.local:
targets.add(self.author) targets.add(self.author)
return targets return targets

View file

@ -172,6 +172,7 @@ class Compose(FormView):
visibility = forms.ChoiceField( visibility = forms.ChoiceField(
choices=[ choices=[
(Post.Visibilities.public, "Public"), (Post.Visibilities.public, "Public"),
(Post.Visibilities.local_only, "Local Only"),
(Post.Visibilities.unlisted, "Unlisted"), (Post.Visibilities.unlisted, "Unlisted"),
(Post.Visibilities.followers, "Followers & Mentioned Only"), (Post.Visibilities.followers, "Followers & Mentioned Only"),
(Post.Visibilities.mentioned, "Mentioned Only"), (Post.Visibilities.mentioned, "Mentioned Only"),
@ -207,7 +208,7 @@ class Compose(FormView):
] = self.request.identity.config_identity.default_post_visibility ] = self.request.identity.config_identity.default_post_visibility
if self.reply_to: if self.reply_to:
initial["reply_to"] = self.reply_to.pk 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} " initial["text"] = f"@{self.reply_to.author.handle} "
return initial return initial

View file

@ -15,6 +15,8 @@
<i class="visibility fa-solid fa-lock" title="Followers Only"></i> <i class="visibility fa-solid fa-lock" title="Followers Only"></i>
{% elif post.visibility == 3 %} {% elif post.visibility == 3 %}
<i class="visibility fa-solid fa-at" title="Mentioned Only"></i> <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 %} {% endif %}
<a href="{{ post.url }}"> <a href="{{ post.url }}">
{% if post.published %} {% if post.published %}

View file

@ -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}

View file

@ -68,11 +68,16 @@ def user() -> User:
@pytest.fixture @pytest.fixture
@pytest.mark.django_db @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. 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( identity = Identity.objects.create(
actor_uri="https://example.com/@test@example.com/", actor_uri="https://example.com/@test@example.com/",
username="test", username="test",
@ -84,9 +89,25 @@ def identity(user):
return identity return identity
@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.fixture
@pytest.mark.django_db @pytest.mark.django_db
def remote_identity(): def remote_identity() -> Identity:
""" """
Creates a basic remote test identity with a domain. Creates a basic remote test identity with a domain.
""" """