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 %}
+
+
+
+ {% 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 %}
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)