mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-02-02 20:32:20 +00:00
Merge pull request #3124 from hughrun/softblock
Allow removing followers and fix follow rejections
This commit is contained in:
commit
4bfa1ca5b8
11 changed files with 110 additions and 14 deletions
|
@ -171,9 +171,19 @@ class Reject(Verb):
|
||||||
type: str = "Reject"
|
type: str = "Reject"
|
||||||
|
|
||||||
def action(self, allow_external_connections=True):
|
def action(self, allow_external_connections=True):
|
||||||
"""reject a follow request"""
|
"""reject a follow or follow request"""
|
||||||
obj = self.object.to_model(save=False, allow_create=False)
|
|
||||||
|
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()
|
obj.reject()
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
|
|
|
@ -65,6 +65,13 @@ class UserRelationship(BookWyrmModel):
|
||||||
base_path = self.user_subject.remote_id
|
base_path = self.user_subject.remote_id
|
||||||
return f"{base_path}#follows/{self.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):
|
class UserFollows(ActivityMixin, UserRelationship):
|
||||||
"""Following a user"""
|
"""Following a user"""
|
||||||
|
@ -105,6 +112,20 @@ class UserFollows(ActivityMixin, UserRelationship):
|
||||||
)
|
)
|
||||||
return obj
|
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):
|
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
"""following a user requires manual or automatic confirmation"""
|
"""following a user requires manual or automatic confirmation"""
|
||||||
|
@ -148,13 +169,6 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
if not manually_approves:
|
if not manually_approves:
|
||||||
self.accept()
|
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):
|
def accept(self, broadcast_only=False):
|
||||||
"""turn this request into the real deal"""
|
"""turn this request into the real deal"""
|
||||||
user = self.user_object
|
user = self.user_object
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% if not minimal %}
|
{% if not minimal %}
|
||||||
<div class="control">
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
5
bookwyrm/templates/snippets/remove_follower_button.html
Normal file
5
bookwyrm/templates/snippets/remove_follower_button.html
Normal file
|
@ -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>
|
|
@ -20,4 +20,9 @@
|
||||||
<li role="menuitem">
|
<li role="menuitem">
|
||||||
{% include 'snippets/block_button.html' with user=user class="is-fullwidth" blocks=False %}
|
{% include 'snippets/block_button.html' with user=user class="is-fullwidth" blocks=False %}
|
||||||
</li>
|
</li>
|
||||||
|
{% if followers_page %}
|
||||||
|
<li role="menuitem">
|
||||||
|
{% include 'snippets/remove_follower_button.html' with user=user class="is-fullwidth" %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -25,6 +25,11 @@
|
||||||
</nav>
|
</nav>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
{% with followers_page=True %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block nullstate %}
|
{% block nullstate %}
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
({{ follow.username }})
|
({{ follow.username }})
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -177,13 +177,39 @@ class FollowViews(TestCase):
|
||||||
user_subject=self.remote_user, user_object=self.local_user
|
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)
|
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
|
# request should be deleted
|
||||||
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)
|
self.assertEqual(models.UserFollowRequest.objects.filter(id=rel.id).count(), 0)
|
||||||
# follow relationship should not exist
|
# follow relationship should not exist
|
||||||
self.assertEqual(models.UserFollows.objects.filter(id=rel.id).count(), 0)
|
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, *_):
|
def test_ostatus_follow_request(self, *_):
|
||||||
"""check ostatus subscribe template loads"""
|
"""check ostatus subscribe template loads"""
|
||||||
request = self.factory.get(
|
request = self.factory.get(
|
||||||
|
|
|
@ -768,6 +768,9 @@ urlpatterns = [
|
||||||
# following
|
# following
|
||||||
re_path(r"^follow/?$", views.follow, name="follow"),
|
re_path(r"^follow/?$", views.follow, name="follow"),
|
||||||
re_path(r"^unfollow/?$", views.unfollow, name="unfollow"),
|
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"^accept-follow-request/?$", views.accept_follow_request),
|
||||||
re_path(r"^delete-follow-request/?$", views.delete_follow_request),
|
re_path(r"^delete-follow-request/?$", views.delete_follow_request),
|
||||||
re_path(r"^ostatus_follow/?$", views.remote_follow, name="remote-follow"),
|
re_path(r"^ostatus_follow/?$", views.remote_follow, name="remote-follow"),
|
||||||
|
|
|
@ -113,6 +113,7 @@ from .feed import DirectMessage, Feed, Replies, Status
|
||||||
from .follow import (
|
from .follow import (
|
||||||
follow,
|
follow,
|
||||||
unfollow,
|
unfollow,
|
||||||
|
remove_follow,
|
||||||
ostatus_follow_request,
|
ostatus_follow_request,
|
||||||
ostatus_follow_success,
|
ostatus_follow_success,
|
||||||
remote_follow,
|
remote_follow,
|
||||||
|
|
|
@ -69,6 +69,33 @@ def unfollow(request):
|
||||||
return redirect("/")
|
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
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def accept_follow_request(request):
|
def accept_follow_request(request):
|
||||||
|
@ -100,7 +127,7 @@ def delete_follow_request(request):
|
||||||
)
|
)
|
||||||
follow_request.raise_not_deletable(request.user)
|
follow_request.raise_not_deletable(request.user)
|
||||||
|
|
||||||
follow_request.delete()
|
follow_request.reject()
|
||||||
return redirect(f"/user/{request.user.localname}")
|
return redirect(f"/user/{request.user.localname}")
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue