From 6b5644ed005fdd682f1d1798bfd3ae4e381ebb82 Mon Sep 17 00:00:00 2001 From: Adam Kelly Date: Fri, 13 Mar 2020 13:13:32 +0000 Subject: [PATCH 1/6] Generate a notification for pending follow requests. --- fedireads/incoming.py | 4 +++- fedireads/models/status.py | 3 ++- fedireads/templates/notifications.html | 4 ++++ init_db.py | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/fedireads/incoming.py b/fedireads/incoming.py index b975707e5..ee0e12e07 100644 --- a/fedireads/incoming.py +++ b/fedireads/incoming.py @@ -229,9 +229,11 @@ def handle_incoming_follow(activity): # Accept, but then do we need to match the activity id? return HttpResponse() - create_notification(to_follow, 'FOLLOW', related_user=user) if not to_follow.manually_approves_followers: + create_notification(to_follow, 'FOLLOW', related_user=user) outgoing.handle_outgoing_accept(user, to_follow, activity) + else: + create_notification(to_follow, 'FOLLOW_REQUEST', related_user=user) return HttpResponse() diff --git a/fedireads/models/status.py b/fedireads/models/status.py index 34715fb9d..b2e838959 100644 --- a/fedireads/models/status.py +++ b/fedireads/models/status.py @@ -107,7 +107,8 @@ class Notification(FedireadsModel): 'FAVORITE', 'REPLY', 'TAG', - 'FOLLOW' + 'FOLLOW', + 'FOLLOW_REQUEST' ] if not self.notification_type in types: raise ValueError('Invalid notitication type') diff --git a/fedireads/templates/notifications.html b/fedireads/templates/notifications.html index 81fd5a413..5ded6af9e 100644 --- a/fedireads/templates/notifications.html +++ b/fedireads/templates/notifications.html @@ -25,6 +25,10 @@ {% elif notification.notification_type == 'FOLLOW' %} {% include 'snippets/username.html' with user=notification.related_user %} followed you + + {% elif notification.notification_type == 'FOLLOW_REQUEST' %} + {% include 'snippets/username.html' with user=notification.related_user %} + sent you a follow request {% endif %} {{ notification.created_date | naturaltime }}

diff --git a/init_db.py b/init_db.py index eb5e2ed89..f659603d7 100644 --- a/init_db.py +++ b/init_db.py @@ -2,7 +2,7 @@ from fedireads.models import User from fedireads.books_manager import get_or_create_book 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)) From 1693473fd22a14238262a9655fc129276aa6fe68 Mon Sep 17 00:00:00 2001 From: Adam Kelly Date: Fri, 13 Mar 2020 13:38:09 +0000 Subject: [PATCH 2/6] Use Enum and constraint for valid notification types. --- .../migrations/0016_auto_20200313_1337.py | 22 ++++++++++++++++ fedireads/models/status.py | 25 ++++++++----------- 2 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 fedireads/migrations/0016_auto_20200313_1337.py diff --git a/fedireads/migrations/0016_auto_20200313_1337.py b/fedireads/migrations/0016_auto_20200313_1337.py new file mode 100644 index 000000000..4f48de45f --- /dev/null +++ b/fedireads/migrations/0016_auto_20200313_1337.py @@ -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'), + ), + ] diff --git a/fedireads/models/status.py b/fedireads/models/status.py index b2e838959..228f48cc2 100644 --- a/fedireads/models/status.py +++ b/fedireads/models/status.py @@ -88,6 +88,9 @@ class Tag(FedireadsModel): unique_together = ('user', 'book', 'name') +NotificationType = models.TextChoices( + 'NotificationType', 'FAVORITE REPLY TAG FOLLOW FOLLOW_REQUEST') + class Notification(FedireadsModel): ''' you've been tagged, liked, followed, etc ''' user = models.ForeignKey('User', on_delete=models.PROTECT) @@ -99,18 +102,12 @@ class Notification(FedireadsModel): related_status = models.ForeignKey( 'Status', on_delete=models.PROTECT, null=True) read = models.BooleanField(default=False) - notification_type = models.CharField(max_length=255) - - def save(self, *args, **kwargs): - # TODO: there's probably a real way to do enums - types = [ - 'FAVORITE', - 'REPLY', - 'TAG', - 'FOLLOW', - 'FOLLOW_REQUEST' + notification_type = models.CharField( + max_length=255, choices=NotificationType.choices) + class Meta: + constraints = [ + models.CheckConstraint( + check=models.Q(notification_type__in=NotificationType.values), + name="notification_type_valid", + ) ] - if not self.notification_type in types: - raise ValueError('Invalid notitication type') - super().save(*args, **kwargs) - From 05f5315b98d208061752ba2183cfec3b5d80ba5f Mon Sep 17 00:00:00 2001 From: Adam Kelly Date: Fri, 13 Mar 2020 14:34:40 +0000 Subject: [PATCH 3/6] List of pending follow requests with accept/delete buttons on own profile. --- fedireads/templates/user.html | 21 +++++++++++++++++++++ fedireads/urls.py | 4 ++++ fedireads/view_actions.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/fedireads/templates/user.html b/fedireads/templates/user.html index 8feb12abc..ec51b9db5 100644 --- a/fedireads/templates/user.html +++ b/fedireads/templates/user.html @@ -20,6 +20,27 @@ {% endif %} + + {% if is_self and user.follower_requests.all %} +
+

Follow Requests

+ {% for requester in user.follower_requests.all %} +
+ {% include 'snippets/username.html' with user=requester show_full=True %} +
+ {% csrf_token %} + + +
+
+ {% csrf_token %} + + +
+
+ {% endfor %} +
+ {% endif %}

Followers

{% for follower in user.followers.all %} diff --git a/fedireads/urls.py b/fedireads/urls.py index d962671b4..f3e6e4a74 100644 --- a/fedireads/urls.py +++ b/fedireads/urls.py @@ -62,4 +62,8 @@ urlpatterns = [ re_path(r'^edit_profile/?$', actions.edit_profile), 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) diff --git a/fedireads/view_actions.py b/fedireads/view_actions.py index 5aa2c8add..d173c80ba 100644 --- a/fedireads/view_actions.py +++ b/fedireads/view_actions.py @@ -163,3 +163,35 @@ def clear_notifications(request): request.user.notification_set.filter(read=True).delete() 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() + + follow_request = models.UserFollowRequest.objects.get(user_subject=requester, user_object=request.user) + # We don't keep a full copy of the follow request, but a minimal copy is good enough for now. + follow_activity = {'id': follow_request.relationship_id} + outgoing.handle_outgoing_accept(requester, request.user, follow_activity) + user_slug = requester.localname if requester.localname \ + else requester.username + return redirect('/user/%s' % user_slug) + +@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) + follow_request.delete() + # Generate a Reject activity here. + except models.UserFollowRequest.DoesNotExist: + pass + + return redirect('/user/%s' % request.user.localname) From 0e34c74647f0731fcc48e341a15f53e8e74a76ea Mon Sep 17 00:00:00 2001 From: Adam Kelly Date: Fri, 13 Mar 2020 14:56:50 +0000 Subject: [PATCH 4/6] Show 'request already sent' while waiting for response. --- fedireads/templates/snippets/follow_button.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fedireads/templates/snippets/follow_button.html b/fedireads/templates/snippets/follow_button.html index 23f9ab565..ac8d4fdfb 100644 --- a/fedireads/templates/snippets/follow_button.html +++ b/fedireads/templates/snippets/follow_button.html @@ -1,4 +1,8 @@ -{% if not request.user in user.followers.all %} +{% if request.user in user.follower_requests.all %} +
+Follow request already sent. +
+{% elif not request.user in user.followers.all %}
{% csrf_token %} From fa57d6cfc74fc1949796c29eb9582ed19420fbb3 Mon Sep 17 00:00:00 2001 From: Adam Kelly Date: Fri, 13 Mar 2020 14:58:30 +0000 Subject: [PATCH 5/6] Include accept/delete buttons in notifications list. --- fedireads/templates/notifications.html | 1 + .../templates/snippets/follow_request_buttons.html | 10 ++++++++++ fedireads/templates/user.html | 11 +---------- 3 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 fedireads/templates/snippets/follow_request_buttons.html diff --git a/fedireads/templates/notifications.html b/fedireads/templates/notifications.html index 5ded6af9e..f19ae9fa8 100644 --- a/fedireads/templates/notifications.html +++ b/fedireads/templates/notifications.html @@ -29,6 +29,7 @@ {% 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 %} {{ notification.created_date | naturaltime }}

diff --git a/fedireads/templates/snippets/follow_request_buttons.html b/fedireads/templates/snippets/follow_request_buttons.html new file mode 100644 index 000000000..97aeb745e --- /dev/null +++ b/fedireads/templates/snippets/follow_request_buttons.html @@ -0,0 +1,10 @@ + + {% csrf_token %} + + + +
+ {% csrf_token %} + + +
diff --git a/fedireads/templates/user.html b/fedireads/templates/user.html index ec51b9db5..a3eeaaf9b 100644 --- a/fedireads/templates/user.html +++ b/fedireads/templates/user.html @@ -27,16 +27,7 @@ {% for requester in user.follower_requests.all %}
{% include 'snippets/username.html' with user=requester show_full=True %} -
- {% csrf_token %} - - -
-
- {% csrf_token %} - - -
+ {% include 'snippets/follow_request_buttons.html' with user=requester %}
{% endfor %}
From 20662a90dd2e2aee18d7095d741c0c741585b585 Mon Sep 17 00:00:00 2001 From: Adam Kelly Date: Fri, 13 Mar 2020 17:04:39 +0000 Subject: [PATCH 6/6] Generate Reject activities. Work on checking incoming / outgoing will work alone. --- fedireads/activitypub/__init__.py | 2 +- fedireads/activitypub/follow.py | 24 ++++++++++++++++-- fedireads/incoming.py | 41 ++++++++++++++++++------------- fedireads/outgoing.py | 13 ++++++---- fedireads/view_actions.py | 21 ++++++++-------- 5 files changed, 66 insertions(+), 35 deletions(-) diff --git a/fedireads/activitypub/__init__.py b/fedireads/activitypub/__init__.py index 50424b8fe..05ac4aedc 100644 --- a/fedireads/activitypub/__init__.py +++ b/fedireads/activitypub/__init__.py @@ -3,6 +3,6 @@ from .actor import get_actor from .collection import get_outbox, get_outbox_page, get_add, get_remove, \ get_following, get_followers 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, \ get_favorite, get_add_tag, get_remove_tag, get_replies_page diff --git a/fedireads/activitypub/follow.py b/fedireads/activitypub/follow.py index 0c5d39382..04a71ef40 100644 --- a/fedireads/activitypub/follow.py +++ b/fedireads/activitypub/follow.py @@ -32,13 +32,33 @@ def get_unfollow(relationship): } -def get_accept(user, request_activity): +def get_accept(user, relationship): ''' accept a follow request ''' return { '@context': 'https://www.w3.org/ns/activitystreams', 'id': '%s#accepts/follows/' % user.absolute_id, 'type': 'Accept', '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, + } + } diff --git a/fedireads/incoming.py b/fedireads/incoming.py index ee0e12e07..2d3b1bdb8 100644 --- a/fedireads/incoming.py +++ b/fedireads/incoming.py @@ -54,6 +54,8 @@ def shared_inbox(request): elif activity['type'] == 'Add': response = handle_incoming_add(activity) + elif activity['type'] == 'Reject': + response = handle_incoming_follow_reject(activity) # TODO: Add, Undo, Remove, etc @@ -218,7 +220,7 @@ def handle_incoming_follow(activity): user = get_or_create_remote_user(activity['actor']) # TODO: allow users to manually approve requests try: - models.UserFollowRequest.objects.create( + request = models.UserFollowRequest.objects.create( user_subject=user, user_object=to_follow, relationship_id=activity['id'] @@ -231,7 +233,7 @@ def handle_incoming_follow(activity): if not to_follow.manually_approves_followers: create_notification(to_follow, 'FOLLOW', related_user=user) - outgoing.handle_outgoing_accept(user, to_follow, activity) + outgoing.handle_outgoing_accept(user, to_follow, request) else: create_notification(to_follow, 'FOLLOW_REQUEST', related_user=user) return HttpResponse() @@ -260,10 +262,29 @@ def handle_incoming_follow_accept(activity): # figure out who they are accepter = get_or_create_remote_user(activity['actor']) - accepter.followers.add(requester) + try: + request = models.UserFollowRequest.objects.get(user_subject=requester, user_object=accepter) + request.delete() + except models.UserFollowRequest.DoesNotExist: + pass + else: + accepter.followers.add(requester) 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): ''' someone did something, good on them ''' user = get_or_create_remote_user(activity['actor']) @@ -331,17 +352,3 @@ def handle_incoming_add(activity): return HttpResponse() return HttpResponse() 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() - diff --git a/fedireads/outgoing.py b/fedireads/outgoing.py index 75e8d7340..6a66c5f6d 100644 --- a/fedireads/outgoing.py +++ b/fedireads/outgoing.py @@ -99,20 +99,23 @@ def handle_outgoing_unfollow(user, to_unfollow): 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 ''' with transaction.atomic(): - follow_request = models.UserFollowRequest.objects.get( - relationship_id=request_activity['id'] - ) relationship = models.UserFollows.from_request(follow_request) follow_request.delete() 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]) 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): ''' a local user is getting a book put on their shelf ''' diff --git a/fedireads/view_actions.py b/fedireads/view_actions.py index d173c80ba..a1e34d979 100644 --- a/fedireads/view_actions.py +++ b/fedireads/view_actions.py @@ -171,13 +171,15 @@ def accept_follow_request(request): except models.User.DoesNotExist: return HttpResponseBadRequest() - follow_request = models.UserFollowRequest.objects.get(user_subject=requester, user_object=request.user) - # We don't keep a full copy of the follow request, but a minimal copy is good enough for now. - follow_activity = {'id': follow_request.relationship_id} - outgoing.handle_outgoing_accept(requester, request.user, follow_activity) - user_slug = requester.localname if requester.localname \ - else requester.username - return redirect('/user/%s' % user_slug) + 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): @@ -189,9 +191,8 @@ def delete_follow_request(request): try: follow_request = models.UserFollowRequest.objects.get(user_subject=requester, user_object=request.user) - follow_request.delete() - # Generate a Reject activity here. except models.UserFollowRequest.DoesNotExist: - pass + return HttpResponseBadRequest() + outgoing.handle_outgoing_reject(requester, request.user, follow_request) return redirect('/user/%s' % request.user.localname)