Merge pull request #85 from cthulahoops/follow_request_acceptance

Follow request acceptance
This commit is contained in:
Mouse Reeve 2020-03-13 10:40:08 -07:00 committed by GitHub
commit ba44566d6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 161 additions and 41 deletions

View file

@ -3,6 +3,6 @@ from .actor import get_actor
from .collection import get_outbox, get_outbox_page, get_add, get_remove, \ from .collection import get_outbox, get_outbox_page, get_add, get_remove, \
get_following, get_followers get_following, get_followers
from .create import get_create from .create import get_create
from .follow import get_follow_request, get_unfollow, get_accept from .follow import get_follow_request, get_unfollow, get_accept, get_reject
from .status import get_review, get_review_article, get_status, get_replies, \ from .status import get_review, get_review_article, get_status, get_replies, \
get_favorite, get_add_tag, get_remove_tag, get_replies_page get_favorite, get_add_tag, get_remove_tag, get_replies_page

View file

@ -32,13 +32,33 @@ def get_unfollow(relationship):
} }
def get_accept(user, request_activity): def get_accept(user, relationship):
''' accept a follow request ''' ''' accept a follow request '''
return { return {
'@context': 'https://www.w3.org/ns/activitystreams', '@context': 'https://www.w3.org/ns/activitystreams',
'id': '%s#accepts/follows/' % user.absolute_id, 'id': '%s#accepts/follows/' % user.absolute_id,
'type': 'Accept', 'type': 'Accept',
'actor': user.actor, 'actor': user.actor,
'object': request_activity, 'object': {
'id': relationship.relationship_id,
'type': 'Follow',
'actor': relationship.user_subject.actor,
'object': relationship.user_object.actor,
}
} }
def get_reject(user, relationship):
''' reject a follow request '''
return {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': '%s#rejects/follows/' % user.absolute_id,
'type': 'Reject',
'actor': user.actor,
'object': {
'id': relationship.relationship_id,
'type': 'Follow',
'actor': relationship.user_subject.actor,
'object': relationship.user_object.actor,
}
}

View file

@ -54,6 +54,8 @@ def shared_inbox(request):
elif activity['type'] == 'Add': elif activity['type'] == 'Add':
response = handle_incoming_add(activity) response = handle_incoming_add(activity)
elif activity['type'] == 'Reject':
response = handle_incoming_follow_reject(activity)
# TODO: Add, Undo, Remove, etc # TODO: Add, Undo, Remove, etc
@ -218,7 +220,7 @@ def handle_incoming_follow(activity):
user = get_or_create_remote_user(activity['actor']) user = get_or_create_remote_user(activity['actor'])
# TODO: allow users to manually approve requests # TODO: allow users to manually approve requests
try: try:
models.UserFollowRequest.objects.create( request = models.UserFollowRequest.objects.create(
user_subject=user, user_subject=user,
user_object=to_follow, user_object=to_follow,
relationship_id=activity['id'] relationship_id=activity['id']
@ -229,9 +231,11 @@ def handle_incoming_follow(activity):
# Accept, but then do we need to match the activity id? # Accept, but then do we need to match the activity id?
return HttpResponse() return HttpResponse()
create_notification(to_follow, 'FOLLOW', related_user=user)
if not to_follow.manually_approves_followers: if not to_follow.manually_approves_followers:
outgoing.handle_outgoing_accept(user, to_follow, activity) create_notification(to_follow, 'FOLLOW', related_user=user)
outgoing.handle_outgoing_accept(user, to_follow, request)
else:
create_notification(to_follow, 'FOLLOW_REQUEST', related_user=user)
return HttpResponse() return HttpResponse()
@ -258,10 +262,29 @@ def handle_incoming_follow_accept(activity):
# figure out who they are # figure out who they are
accepter = get_or_create_remote_user(activity['actor']) accepter = get_or_create_remote_user(activity['actor'])
try:
request = models.UserFollowRequest.objects.get(user_subject=requester, user_object=accepter)
request.delete()
except models.UserFollowRequest.DoesNotExist:
pass
else:
accepter.followers.add(requester) accepter.followers.add(requester)
return HttpResponse() return HttpResponse()
def handle_incoming_follow_reject(activity):
''' someone is rejecting a follow request '''
requester = models.User.objects.get(actor=activity['object']['actor'])
rejecter = get_or_create_remote_user(activity['actor'])
try:
request = models.UserFollowRequest.objects.get(user_subject=requester, user_object=rejecter)
request.delete()
except models.UserFollowRequest.DoesNotExist:
pass
return HttpResponse()
def handle_incoming_create(activity): def handle_incoming_create(activity):
''' someone did something, good on them ''' ''' someone did something, good on them '''
user = get_or_create_remote_user(activity['actor']) user = get_or_create_remote_user(activity['actor'])
@ -329,17 +352,3 @@ def handle_incoming_add(activity):
return HttpResponse() return HttpResponse()
return HttpResponse() return HttpResponse()
return HttpResponseNotFound() return HttpResponseNotFound()
def handle_incoming_accept(activity):
''' someone is accepting a follow request '''
# our local user
user = models.User.objects.get(actor=activity['actor'])
# the person our local user wants to follow, who said yes
followed = get_or_create_remote_user(activity['object']['actor'])
# save this relationship in the db
followed.followers.add(user)
return HttpResponse()

View file

@ -0,0 +1,22 @@
# Generated by Django 3.0.3 on 2020-03-13 13:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fedireads', '0015_auto_20200311_1212'),
]
operations = [
migrations.AlterField(
model_name='notification',
name='notification_type',
field=models.CharField(choices=[('FAVORITE', 'Favorite'), ('REPLY', 'Reply'), ('TAG', 'Tag'), ('FOLLOW', 'Follow'), ('FOLLOW_REQUEST', 'Follow Request')], max_length=255),
),
migrations.AddConstraint(
model_name='notification',
constraint=models.CheckConstraint(check=models.Q(notification_type__in=['FAVORITE', 'REPLY', 'TAG', 'FOLLOW', 'FOLLOW_REQUEST']), name='notification_type_valid'),
),
]

View file

@ -88,6 +88,9 @@ class Tag(FedireadsModel):
unique_together = ('user', 'book', 'name') unique_together = ('user', 'book', 'name')
NotificationType = models.TextChoices(
'NotificationType', 'FAVORITE REPLY TAG FOLLOW FOLLOW_REQUEST')
class Notification(FedireadsModel): class Notification(FedireadsModel):
''' you've been tagged, liked, followed, etc ''' ''' you've been tagged, liked, followed, etc '''
user = models.ForeignKey('User', on_delete=models.PROTECT) user = models.ForeignKey('User', on_delete=models.PROTECT)
@ -99,17 +102,12 @@ class Notification(FedireadsModel):
related_status = models.ForeignKey( related_status = models.ForeignKey(
'Status', on_delete=models.PROTECT, null=True) 'Status', on_delete=models.PROTECT, null=True)
read = models.BooleanField(default=False) read = models.BooleanField(default=False)
notification_type = models.CharField(max_length=255) notification_type = models.CharField(
max_length=255, choices=NotificationType.choices)
def save(self, *args, **kwargs): class Meta:
# TODO: there's probably a real way to do enums constraints = [
types = [ models.CheckConstraint(
'FAVORITE', check=models.Q(notification_type__in=NotificationType.values),
'REPLY', name="notification_type_valid",
'TAG', )
'FOLLOW'
] ]
if not self.notification_type in types:
raise ValueError('Invalid notitication type')
super().save(*args, **kwargs)

View file

@ -99,20 +99,23 @@ def handle_outgoing_unfollow(user, to_unfollow):
raise(error['error']) raise(error['error'])
def handle_outgoing_accept(user, to_follow, request_activity): def handle_outgoing_accept(user, to_follow, follow_request):
''' send an acceptance message to a follow request ''' ''' send an acceptance message to a follow request '''
with transaction.atomic(): with transaction.atomic():
follow_request = models.UserFollowRequest.objects.get(
relationship_id=request_activity['id']
)
relationship = models.UserFollows.from_request(follow_request) relationship = models.UserFollows.from_request(follow_request)
follow_request.delete() follow_request.delete()
relationship.save() relationship.save()
activity = activitypub.get_accept(to_follow, request_activity) activity = activitypub.get_accept(to_follow, follow_request)
recipient = get_recipients(to_follow, 'direct', direct_recipients=[user]) recipient = get_recipients(to_follow, 'direct', direct_recipients=[user])
broadcast(to_follow, activity, recipient) broadcast(to_follow, activity, recipient)
def handle_outgoing_reject(user, to_follow, relationship):
relationship.delete()
activity = activitypub.get_reject(to_follow, relationship)
recipient = get_recipients(to_follow, 'direct', direct_recipients=[user])
broadcast(to_follow, activity, recipient)
def handle_shelve(user, book, shelf): def handle_shelve(user, book, shelf):
''' a local user is getting a book put on their shelf ''' ''' a local user is getting a book put on their shelf '''

View file

@ -25,6 +25,11 @@
{% elif notification.notification_type == 'FOLLOW' %} {% elif notification.notification_type == 'FOLLOW' %}
{% include 'snippets/username.html' with user=notification.related_user %} {% include 'snippets/username.html' with user=notification.related_user %}
followed you followed you
{% elif notification.notification_type == 'FOLLOW_REQUEST' %}
{% include 'snippets/username.html' with user=notification.related_user %}
sent you a follow request
{% include 'snippets/follow_request_buttons.html' with user=notification.related_user %}
{% endif %} {% endif %}
<small>{{ notification.created_date | naturaltime }}</small> <small>{{ notification.created_date | naturaltime }}</small>
</p> </p>

View file

@ -1,4 +1,8 @@
{% if not request.user in user.followers.all %} {% if request.user in user.follower_requests.all %}
<div>
Follow request already sent.
</div>
{% elif not request.user in user.followers.all %}
<form action="/follow/" method="post"> <form action="/follow/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"></input> <input type="hidden" name="user" value="{{ user.username }}"></input>

View file

@ -0,0 +1,10 @@
<form action="/accept_follow_request/" method="POST">
{% csrf_token %}
<input type=hidden name="user" value="{{ user.username }}">
<input type=submit value="Accept">
</form>
<form action="/delete_follow_request/" method="POST">
{% csrf_token %}
<input type=hidden name="user" value="{{ user.username }}">
<input type=submit value="Delete">
</form>

View file

@ -20,6 +20,18 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if is_self and user.follower_requests.all %}
<div>
<h2>Follow Requests</h2>
{% for requester in user.follower_requests.all %}
<div>
{% include 'snippets/username.html' with user=requester show_full=True %}
{% include 'snippets/follow_request_buttons.html' with user=requester %}
</div>
{% endfor %}
</div>
{% endif %}
<div> <div>
<h2>Followers</h2> <h2>Followers</h2>
{% for follower in user.followers.all %} {% for follower in user.followers.all %}

View file

@ -62,4 +62,8 @@ urlpatterns = [
re_path(r'^edit_profile/?$', actions.edit_profile), re_path(r'^edit_profile/?$', actions.edit_profile),
re_path(r'^clear-notifications/?$', actions.clear_notifications), re_path(r'^clear-notifications/?$', actions.clear_notifications),
re_path(r'^accept_follow_request/?$', actions.accept_follow_request),
re_path(r'^delete_follow_request/?$', actions.delete_follow_request),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -163,3 +163,36 @@ def clear_notifications(request):
request.user.notification_set.filter(read=True).delete() request.user.notification_set.filter(read=True).delete()
return redirect('/notifications') return redirect('/notifications')
@login_required
def accept_follow_request(request):
username = request.POST['user']
try:
requester = get_user_from_username(username)
except models.User.DoesNotExist:
return HttpResponseBadRequest()
try:
follow_request = models.UserFollowRequest.objects.get(user_subject=requester, user_object=request.user)
except models.UserFollowRequest.DoesNotExist:
# Request already dealt with.
pass
else:
outgoing.handle_outgoing_accept(requester, request.user, follow_request)
return redirect('/user/%s' % request.user.localname)
@login_required
def delete_follow_request(request):
username = request.POST['user']
try:
requester = get_user_from_username(username)
except models.User.DoesNotExist:
return HttpResponseBadRequest()
try:
follow_request = models.UserFollowRequest.objects.get(user_subject=requester, user_object=request.user)
except models.UserFollowRequest.DoesNotExist:
return HttpResponseBadRequest()
outgoing.handle_outgoing_reject(requester, request.user, follow_request)
return redirect('/user/%s' % request.user.localname)

View file

@ -2,7 +2,7 @@ from fedireads.models import User
from fedireads.books_manager import get_or_create_book from fedireads.books_manager import get_or_create_book
User.objects.create_user('mouse', 'mouse.reeve@gmail.com', 'password123') User.objects.create_user('mouse', 'mouse.reeve@gmail.com', 'password123')
User.objects.create_user('rat', 'rat@rat.com', 'ratword') User.objects.create_user('rat', 'rat@rat.com', 'ratword', manually_approves_followers=True)
User.objects.get(id=1).followers.add(User.objects.get(id=2)) User.objects.get(id=1).followers.add(User.objects.get(id=2))