Skip to content

Commit

Permalink
Merge pull request #3124 from hughrun/softblock
Browse files Browse the repository at this point in the history
Allow removing followers and fix follow rejections
  • Loading branch information
mouse-reeve authored Dec 12, 2023
2 parents 799f842 + c6dea25 commit 4bfa1ca
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 14 deletions.
16 changes: 13 additions & 3 deletions bookwyrm/activitypub/verbs.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,19 @@ class Reject(Verb):
type: str = "Reject"

def action(self, allow_external_connections=True):
"""reject a follow request"""
obj = self.object.to_model(save=False, allow_create=False)
obj.reject()
"""reject a follow or follow request"""

for model_name in ["UserFollowRequest", "UserFollows", None]:
model = apps.get_model(f"bookwyrm.{model_name}") if model_name else None
if obj := self.object.to_model(
model=model,
save=False,
allow_create=False,
allow_external_connections=allow_external_connections,
):
# Reject the first model that can be built.
obj.reject()
break


@dataclass(init=False)
Expand Down
28 changes: 21 additions & 7 deletions bookwyrm/models/relationship.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ def get_remote_id(self):
base_path = self.user_subject.remote_id
return f"{base_path}#follows/{self.id}"

def get_accept_reject_id(self, status):
"""get id for sending an accept or reject of a local user"""

base_path = self.user_object.remote_id
status_id = self.id or 0
return f"{base_path}#{status}/{status_id}"


class UserFollows(ActivityMixin, UserRelationship):
"""Following a user"""
Expand Down Expand Up @@ -105,6 +112,20 @@ def from_request(cls, follow_request):
)
return obj

def reject(self):
"""generate a Reject for this follow. This would normally happen
when a user deletes a follow they previously accepted"""

if self.user_object.local:
activity = activitypub.Reject(
id=self.get_accept_reject_id(status="rejects"),
actor=self.user_object.remote_id,
object=self.to_activity(),
).serialize()
self.broadcast(activity, self.user_object)

self.delete()


class UserFollowRequest(ActivitypubMixin, UserRelationship):
"""following a user requires manual or automatic confirmation"""
Expand Down Expand Up @@ -148,13 +169,6 @@ def save(self, *args, broadcast=True, **kwargs): # pylint: disable=arguments-di
if not manually_approves:
self.accept()

def get_accept_reject_id(self, status):
"""get id for sending an accept or reject of a local user"""

base_path = self.user_object.remote_id
status_id = self.id or 0
return f"{base_path}#{status}/{status_id}"

def accept(self, broadcast_only=False):
"""turn this request into the real deal"""
user = self.user_object
Expand Down
2 changes: 1 addition & 1 deletion bookwyrm/templates/snippets/follow_button.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
</div>
{% if not minimal %}
<div class="control">
{% include 'snippets/user_options.html' with user=user class="is-small" %}
{% include 'snippets/user_options.html' with user=user followers_page=followers_page class="is-small" %}
</div>
{% endif %}
</div>
Expand Down
5 changes: 5 additions & 0 deletions bookwyrm/templates/snippets/remove_follower_button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% load i18n %}
<form name="remove" method="post" action="/remove-follow/{{ user.id }}">
{% csrf_token %}
<button class="button is-danger is-light is-small {{ class }}" type="submit">{% trans "Remove" %}</button>
</form>
5 changes: 5 additions & 0 deletions bookwyrm/templates/snippets/user_options.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@
<li role="menuitem">
{% include 'snippets/block_button.html' with user=user class="is-fullwidth" blocks=False %}
</li>
{% if followers_page %}
<li role="menuitem">
{% include 'snippets/remove_follower_button.html' with user=user class="is-fullwidth" %}
</li>
{% endif %}
{% endblock %}
5 changes: 5 additions & 0 deletions bookwyrm/templates/user/relationships/followers.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ <h1 class="title">
</nav>
{% endblock %}

{% block panel %}
{% with followers_page=True %}
{{ block.super }}
{% endwith %}
{% endblock %}

{% block nullstate %}
<div>
Expand Down
2 changes: 1 addition & 1 deletion bookwyrm/templates/user/relationships/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
({{ follow.username }})
</div>
<div class="column is-narrow">
{% include 'snippets/follow_button.html' with user=follow %}
{% include 'snippets/follow_button.html' with user=follow followers_page=followers_page %}
</div>
</div>
{% endfor %}
Expand Down
28 changes: 27 additions & 1 deletion bookwyrm/tests/views/test_follow.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,39 @@ def test_handle_reject(self, *_):
user_subject=self.remote_user, user_object=self.local_user
)

with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as broadcast_mock:
views.delete_follow_request(request)
# did we send the reject activity?
activity = json.loads(broadcast_mock.call_args[1]["args"][1])
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["object"], rel.user_object.remote_id)
self.assertEqual(activity["type"], "Reject")
# request should be deleted
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)
# follow relationship should not exist
self.assertEqual(models.UserFollows.objects.filter(id=rel.id).count(), 0)

def test_handle_reject_existing(self, *_):
"""reject a follow previously approved"""
request = self.factory.post("", {"user": self.remote_user.username})
request.user = self.local_user
rel = models.UserFollows.objects.create(
user_subject=self.remote_user, user_object=self.local_user
)
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
) as broadcast_mock:
views.remove_follow(request, self.remote_user.id)
# did we send the reject activity?
activity = json.loads(broadcast_mock.call_args[1]["args"][1])
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(activity["object"]["object"], rel.user_object.remote_id)
self.assertEqual(activity["type"], "Reject")
# follow relationship should not exist
self.assertEqual(models.UserFollows.objects.filter(id=rel.id).count(), 0)

def test_ostatus_follow_request(self, *_):
"""check ostatus subscribe template loads"""
request = self.factory.get(
Expand Down
3 changes: 3 additions & 0 deletions bookwyrm/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,9 @@
# following
re_path(r"^follow/?$", views.follow, name="follow"),
re_path(r"^unfollow/?$", views.unfollow, name="unfollow"),
re_path(
r"^remove-follow/(?P<user_id>\d+)/?$", views.remove_follow, name="remove-follow"
),
re_path(r"^accept-follow-request/?$", views.accept_follow_request),
re_path(r"^delete-follow-request/?$", views.delete_follow_request),
re_path(r"^ostatus_follow/?$", views.remote_follow, name="remote-follow"),
Expand Down
1 change: 1 addition & 0 deletions bookwyrm/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
from .follow import (
follow,
unfollow,
remove_follow,
ostatus_follow_request,
ostatus_follow_success,
remote_follow,
Expand Down
29 changes: 28 additions & 1 deletion bookwyrm/views/follow.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,33 @@ def unfollow(request):
return redirect("/")


@login_required
@require_POST
def remove_follow(request, user_id):
"""remove a previously approved follower without blocking them"""

to_remove = get_object_or_404(models.User, id=user_id)

try:
models.UserFollows.objects.get(
user_subject=to_remove, user_object=request.user
).reject()
except models.UserFollows.DoesNotExist:
clear_cache(to_remove, request.user)

try:
models.UserFollowRequest.objects.get(
user_subject=to_remove, user_object=request.user
).reject()
except models.UserFollowRequest.DoesNotExist:
clear_cache(to_remove, request.user)

if is_api_request(request):
return HttpResponse()

return redirect(f"{request.user.local_path}/followers")


@login_required
@require_POST
def accept_follow_request(request):
Expand Down Expand Up @@ -100,7 +127,7 @@ def delete_follow_request(request):
)
follow_request.raise_not_deletable(request.user)

follow_request.delete()
follow_request.reject()
return redirect(f"/user/{request.user.localname}")


Expand Down

0 comments on commit 4bfa1ca

Please sign in to comment.