mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-10 10:59:30 +00:00
Allow removing followers and fix follow rejections
* adds the ability to remove a user from your followers list * fixes verbs.Reject to process reject activities for previously accepted follows in both directions fixes #2635
This commit is contained in:
parent
06568aab88
commit
2ba0e3d7ff
9 changed files with 120 additions and 21 deletions
|
@ -171,8 +171,36 @@ 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)
|
|
||||||
|
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()
|
obj.reject()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -330,18 +330,30 @@ let BookWyrm = new (class {
|
||||||
|
|
||||||
const bookwyrm = this;
|
const bookwyrm = this;
|
||||||
const form = event.currentTarget;
|
const form = event.currentTarget;
|
||||||
|
const formAction = event.submitter.getAttribute("formaction") || form.action;
|
||||||
const relatedforms = document.querySelectorAll(`.${form.dataset.id}`);
|
const relatedforms = document.querySelectorAll(`.${form.dataset.id}`);
|
||||||
|
|
||||||
// Toggle class on all related forms.
|
// Toggle class on all related forms.
|
||||||
relatedforms.forEach((relatedForm) =>
|
if (formAction == "/remove-follow") {
|
||||||
bookwyrm.addRemoveClass(
|
// Remove ALL follow/unfollow/remote buttons
|
||||||
relatedForm,
|
relatedforms.forEach((relatedForm) => relatedForm.classList.add("is-hidden"));
|
||||||
"is-hidden",
|
|
||||||
relatedForm.className.indexOf("is-hidden") == -1
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
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.
|
// @todo Display a notification in the UI instead.
|
||||||
console.warn("Request failed:", error);
|
console.warn("Request failed:", error);
|
||||||
});
|
});
|
||||||
|
@ -353,8 +365,8 @@ let BookWyrm = new (class {
|
||||||
* @param {object} form - Form to be submitted
|
* @param {object} form - Form to be submitted
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
ajaxPost(form) {
|
ajaxPost(target, form) {
|
||||||
return fetch(form.action, {
|
return fetch(target, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: new FormData(form),
|
body: new FormData(form),
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
|
|
||||||
<div class="field{% if not minimal %} has-addons{% else %} mb-0{% endif %}">
|
<div class="field{% if not minimal %} has-addons{% else %} mb-0{% endif %}">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
{% if not followers_page %}
|
||||||
<form action="{% url 'follow' %}" method="POST" class="interaction follow_{{ user.id }} {% if relationship.is_following or relationship.is_follow_pending %}is-hidden{%endif %}" data-id="follow_{{ user.id }}">
|
<form action="{% url 'follow' %}" method="POST" class="interaction follow_{{ user.id }} {% if relationship.is_following or relationship.is_follow_pending %}is-hidden{%endif %}" data-id="follow_{{ user.id }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="user" value="{{ user.username }}">
|
<input type="hidden" name="user" value="{{ user.username }}">
|
||||||
|
@ -23,13 +24,22 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<form action="{% url 'unfollow' %}" method="POST" class="interaction follow_{{ user.id }} {% if not relationship.is_following and not relationship.is_follow_pending %}is-hidden{%endif %}" data-id="follow_{{ user.id }}">
|
{% endif %}
|
||||||
|
<form action="{% url 'unfollow' %}" method="POST" class="interaction follow_{{ user.id }}" data-id="follow_{{ user.id }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="user" value="{{ user.username }}">
|
<input type="hidden" name="user" value="{{ user.username }}">
|
||||||
{% if relationship.is_follow_pending %}
|
{% if relationship.is_follow_pending %}
|
||||||
<button class="button is-small is-danger is-light" type="submit">
|
<button class="button is-small is-danger is-light" type="submit">
|
||||||
{% trans "Undo follow request" %}
|
{% trans "Undo follow request" %}
|
||||||
</button>
|
</button>
|
||||||
|
{% elif followers_page %}
|
||||||
|
<button class="button is-small is-danger is-light" type="submit" formaction="{% url 'remove-follow' %}">
|
||||||
|
{% if show_username %}
|
||||||
|
{% blocktrans with username=user.localname %}Remove @{{ username }}{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Remove" %}
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="button is-small is-danger is-light" type="submit">
|
<button class="button is-small is-danger is-light" type="submit">
|
||||||
{% if show_username %}
|
{% if show_username %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -763,6 +763,7 @@ 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/?$", 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,34 @@ def unfollow(request):
|
||||||
return redirect("/")
|
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
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def accept_follow_request(request):
|
def accept_follow_request(request):
|
||||||
|
|
Loading…
Reference in a new issue