diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index 00c9524fe..2753104b2 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -171,8 +171,36 @@ 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) + """reject a follow or follow request""" + + if self.object.type == "Follow": + model = apps.get_model("bookwyrm.UserFollowRequest") + obj = self.object.to_model( + model=model, + save=False, + allow_create=False, + allow_external_connections=allow_external_connections, + ) + if not obj: + # This is a deletion (soft-block) of an accepted follow + model = apps.get_model("bookwyrm.UserFollows") + obj = self.object.to_model( + model=model, + save=False, + allow_create=False, + allow_external_connections=allow_external_connections, + ) + else: + # it's something else + obj = self.object.to_model( + model=model, + save=False, + allow_create=False, + allow_external_connections=allow_external_connections, + ) + if not obj: + # if we don't have the object, we can't reject it. + return obj.reject() diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index 7af6ad5ab..3386a02dc 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -65,6 +65,13 @@ class UserRelationship(BookWyrmModel): 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""" @@ -105,6 +112,20 @@ class UserFollows(ActivityMixin, UserRelationship): ) 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""" @@ -148,13 +169,6 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): 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 diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index a2351a98c..7093219e5 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -330,18 +330,30 @@ let BookWyrm = new (class { const bookwyrm = this; const form = event.currentTarget; + const formAction = event.submitter.getAttribute("formaction") || form.action; const relatedforms = document.querySelectorAll(`.${form.dataset.id}`); // Toggle class on all related forms. - relatedforms.forEach((relatedForm) => - bookwyrm.addRemoveClass( - relatedForm, - "is-hidden", - relatedForm.className.indexOf("is-hidden") == -1 - ) - ); + if (formAction == "/remove-follow") { + // Remove ALL follow/unfollow/remote buttons + relatedforms.forEach((relatedForm) => relatedForm.classList.add("is-hidden")); - this.ajaxPost(form).catch((error) => { + // Remove orphaned user-options dropdown + const parent = form.parentElement; + const next = parent.nextElementSibling; + + next.classList.add("is-hidden"); + } else { + relatedforms.forEach((relatedForm) => + bookwyrm.addRemoveClass( + relatedForm, + "is-hidden", + relatedForm.className.indexOf("is-hidden") == -1 + ) + ); + } + + this.ajaxPost(formAction, form).catch((error) => { // @todo Display a notification in the UI instead. console.warn("Request failed:", error); }); @@ -353,8 +365,8 @@ let BookWyrm = new (class { * @param {object} form - Form to be submitted * @return {Promise} */ - ajaxPost(form) { - return fetch(form.action, { + ajaxPost(target, form) { + return fetch(target, { method: "POST", body: new FormData(form), headers: { diff --git a/bookwyrm/templates/snippets/follow_button.html b/bookwyrm/templates/snippets/follow_button.html index 2bde47f58..5c0839065 100644 --- a/bookwyrm/templates/snippets/follow_button.html +++ b/bookwyrm/templates/snippets/follow_button.html @@ -12,6 +12,7 @@
+ {% if not followers_page %} -
{% endfor %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 8541f4fb6..ab1dca378 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -763,6 +763,7 @@ urlpatterns = [ # following re_path(r"^follow/?$", views.follow, name="follow"), re_path(r"^unfollow/?$", views.unfollow, name="unfollow"), + re_path(r"^remove-follow/?$", 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"), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 2d2e97f52..0d7af9d68 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -113,6 +113,7 @@ from .feed import DirectMessage, Feed, Replies, Status from .follow import ( follow, unfollow, + remove_follow, ostatus_follow_request, ostatus_follow_success, remote_follow, diff --git a/bookwyrm/views/follow.py b/bookwyrm/views/follow.py index 0090cbe32..f9a09e2c9 100644 --- a/bookwyrm/views/follow.py +++ b/bookwyrm/views/follow.py @@ -69,6 +69,34 @@ def unfollow(request): return redirect("/") +@login_required +@require_POST +def remove_follow(request): + """remove a previously approved follower without blocking them""" + + username = request.POST["user"] + to_remove = get_user_from_username(request.user, username) + + 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() + # this is handled with ajax so it shouldn't really matter + return redirect("/") + + @login_required @require_POST def accept_follow_request(request):