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 b975707e5..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'] @@ -229,9 +231,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: - 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() @@ -258,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']) @@ -329,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/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 34715fb9d..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,17 +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' + 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) - 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/templates/notifications.html b/fedireads/templates/notifications.html index 81fd5a413..f19ae9fa8 100644 --- a/fedireads/templates/notifications.html +++ b/fedireads/templates/notifications.html @@ -25,6 +25,11 @@ {% 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 + {% include 'snippets/follow_request_buttons.html' with user=notification.related_user %} {% endif %} {{ notification.created_date | naturaltime }}

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 %} 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 8feb12abc..a3eeaaf9b 100644 --- a/fedireads/templates/user.html +++ b/fedireads/templates/user.html @@ -20,6 +20,18 @@ {% 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 %} + {% include 'snippets/follow_request_buttons.html' with user=requester %} +
+ {% 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..a1e34d979 100644 --- a/fedireads/view_actions.py +++ b/fedireads/view_actions.py @@ -163,3 +163,36 @@ 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() + + 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) 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))