diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 103b24fca..562225e79 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -9,7 +9,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST import requests -from bookwyrm import activitypub, models, views +from bookwyrm import activitypub, models from bookwyrm import status as status_builder from bookwyrm.tasks import app from bookwyrm.signatures import Signature @@ -144,7 +144,7 @@ def handle_follow(activity): related_user=relationship.user_subject ) if not manually_approves: - views.handle_accept(relationship) + relationship.accept() @app.task diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index 7d5b5beb2..0168dec83 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -366,12 +366,14 @@ class ActivityMixin(ActivitypubMixin): def save(self, *args, **kwargs): ''' broadcast activity ''' super().save(*args, **kwargs) - self.broadcast(self.to_activity(), self.user) + user = self.user if hasattr(self, 'user') else self.user_subject + self.broadcast(self.to_activity(), user) def delete(self, *args, **kwargs): ''' nevermind, undo that activity ''' - self.broadcast(self.to_undo_activity(), self.user) + user = self.user if hasattr(self, 'user') else self.user_subject + self.broadcast(self.to_undo_activity(), user) super().delete(*args, **kwargs) diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index 712f607b6..2ff0da592 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -1,15 +1,15 @@ ''' defines relationships between users ''' -from django.db import models +from django.db import models, transaction from django.db.models import Q from django.dispatch import receiver from bookwyrm import activitypub -from .activitypub_mixin import ActivityMixin +from .activitypub_mixin import ActivitypubMixin, ActivityMixin from .base_model import BookWyrmModel from . import fields -class UserRelationship(ActivityMixin, BookWyrmModel): +class UserRelationship(BookWyrmModel): ''' many-to-many through table for followers ''' user_subject = fields.ForeignKey( 'User', @@ -24,6 +24,11 @@ class UserRelationship(ActivityMixin, BookWyrmModel): activitypub_field='object', ) + @property + def privacy(self): + ''' all relationships are handled directly with the participants ''' + return 'direct' + class Meta: ''' relationships should be unique ''' abstract = True @@ -38,8 +43,6 @@ class UserRelationship(ActivityMixin, BookWyrmModel): ) ] - activity_serializer = activitypub.Follow - def get_remote_id(self, status=None):# pylint: disable=arguments-differ ''' use shelf identifier in remote_id ''' status = status or 'follows' @@ -47,55 +50,73 @@ class UserRelationship(ActivityMixin, BookWyrmModel): return '%s#%s/%d' % (base_path, status, self.id) - def to_accept_activity(self): - ''' generate an Accept for this follow request ''' - return activitypub.Accept( - id=self.get_remote_id(status='accepts'), - actor=self.user_object.remote_id, - object=self.to_activity() - ).serialize() - - - def to_reject_activity(self): - ''' generate a Reject for this follow request ''' - return activitypub.Reject( - id=self.get_remote_id(status='rejects'), - actor=self.user_object.remote_id, - object=self.to_activity() - ).serialize() - - -class UserFollows(UserRelationship): +class UserFollows(ActivitypubMixin, UserRelationship): ''' Following a user ''' status = 'follows' + activity_serializer = activitypub.Follow + @classmethod def from_request(cls, follow_request): ''' converts a follow request into a follow relationship ''' - return cls( + return cls.objects.create( user_subject=follow_request.user_subject, user_object=follow_request.user_object, remote_id=follow_request.remote_id, ) -class UserFollowRequest(UserRelationship): +class UserFollowRequest(ActivitypubMixin, UserRelationship): ''' following a user requires manual or automatic confirmation ''' status = 'follow_request' + activity_serializer = activitypub.Follow def save(self, *args, **kwargs): - ''' make sure the follow relationship doesn't already exist ''' + ''' make sure the follow or block relationship doesn't already exist ''' try: UserFollows.objects.get( user_subject=self.user_subject, user_object=self.user_object ) + UserBlocks.objects.get( + user_subject=self.user_subject, + user_object=self.user_object + ) return None - except UserFollows.DoesNotExist: - return super().save(*args, **kwargs) + except (UserFollows.DoesNotExist, UserBlocks.DoesNotExist): + super().save(*args, **kwargs) + if self.user_subject.local and not self.user_object.local: + self.broadcast(self.to_activity(), self.user_subject) -class UserBlocks(UserRelationship): + def accept(self): + ''' turn this request into the real deal''' + user = self.user_object + activity = activitypub.Accept( + id=self.get_remote_id(status='accepts'), + actor=self.user_object.remote_id, + object=self.to_activity() + ).serialize() + with transaction.atomic(): + UserFollows.from_request(self) + self.delete() + + self.broadcast(activity, user) + + + def reject(self): + ''' generate a Reject for this follow request ''' + user = self.user_object + activity = activitypub.Reject( + id=self.get_remote_id(status='rejects'), + actor=self.user_object.remote_id, + object=self.to_activity() + ).serialize() + self.delete() + self.broadcast(activity, user) + + +class UserBlocks(ActivityMixin, UserRelationship): ''' prevent another user from following you and seeing your posts ''' status = 'blocks' activity_serializer = activitypub.Block diff --git a/bookwyrm/tests/models/test_relationship_models.py b/bookwyrm/tests/models/test_relationship_models.py index 56f37e1e8..c8d80262b 100644 --- a/bookwyrm/tests/models/test_relationship_models.py +++ b/bookwyrm/tests/models/test_relationship_models.py @@ -6,7 +6,9 @@ from bookwyrm import models class Relationship(TestCase): + ''' following, blocking, stuff like that ''' def setUp(self): + ''' we need some users for this ''' with patch('bookwyrm.models.user.set_remote_server.delay'): self.remote_user = models.User.objects.create_user( 'rat', 'rat@rat.com', 'ratword', @@ -22,65 +24,27 @@ class Relationship(TestCase): self.local_user.save(broadcast=False) def test_user_follows(self): - rel = models.UserFollows.objects.create( - user_subject=self.local_user, - user_object=self.remote_user - ) - - self.assertEqual( - rel.remote_id, - 'http://local.com/user/mouse#follows/%d' % rel.id - ) + ''' create a follow relationship ''' + with patch('bookwyrm.models.activitypub_mixin.ActivityMixin.broadcast'): + rel = models.UserFollows.objects.create( + user_subject=self.local_user, + user_object=self.remote_user + ) activity = rel.to_activity() self.assertEqual(activity['id'], rel.remote_id) self.assertEqual(activity['actor'], self.local_user.remote_id) self.assertEqual(activity['object'], self.remote_user.remote_id) - def test_user_follow_accept_serialization(self): - rel = models.UserFollows.objects.create( - user_subject=self.local_user, - user_object=self.remote_user - ) - - self.assertEqual( - rel.remote_id, - 'http://local.com/user/mouse#follows/%d' % rel.id - ) - accept = rel.to_accept_activity() - self.assertEqual(accept['type'], 'Accept') - self.assertEqual( - accept['id'], - 'http://local.com/user/mouse#accepts/%d' % rel.id - ) - self.assertEqual(accept['actor'], self.remote_user.remote_id) - self.assertEqual(accept['object']['id'], rel.remote_id) - self.assertEqual(accept['object']['actor'], self.local_user.remote_id) - self.assertEqual(accept['object']['object'], self.remote_user.remote_id) - - def test_user_follow_reject_serialization(self): - rel = models.UserFollows.objects.create( - user_subject=self.local_user, - user_object=self.remote_user - ) - - self.assertEqual( - rel.remote_id, - 'http://local.com/user/mouse#follows/%d' % rel.id - ) - reject = rel.to_reject_activity() - self.assertEqual(reject['type'], 'Reject') - self.assertEqual( - reject['id'], - 'http://local.com/user/mouse#rejects/%d' % rel.id - ) - self.assertEqual(reject['actor'], self.remote_user.remote_id) - self.assertEqual(reject['object']['id'], rel.remote_id) - self.assertEqual(reject['object']['actor'], self.local_user.remote_id) - self.assertEqual(reject['object']['object'], self.remote_user.remote_id) - def test_user_follows_from_request(self): + ''' convert a follow request into a follow ''' + def mock_broadcast(_, activity, user): + ''' introspect what's being sent out ''' + self.assertEqual(user.remote_id, self.local_user.remote_id) + self.assertEqual(activity['type'], 'Follow') + + models.UserFollowRequest.broadcast = mock_broadcast request = models.UserFollowRequest.objects.create( user_subject=self.local_user, user_object=self.remote_user @@ -102,11 +66,14 @@ class Relationship(TestCase): def test_user_follows_from_request_custom_remote_id(self): - request = models.UserFollowRequest.objects.create( - user_subject=self.local_user, - user_object=self.remote_user, - remote_id='http://antoher.server/sdkfhskdjf/23' - ) + ''' store a specific remote id for a relationship provided by remote ''' + with patch( + 'bookwyrm.models.activitypub_mixin.ActivitypubMixin.broadcast'): + request = models.UserFollowRequest.objects.create( + user_subject=self.local_user, + user_object=self.remote_user, + remote_id='http://antoher.server/sdkfhskdjf/23' + ) self.assertEqual( request.remote_id, 'http://antoher.server/sdkfhskdjf/23' @@ -121,3 +88,61 @@ class Relationship(TestCase): self.assertEqual(rel.status, 'follows') self.assertEqual(rel.user_subject, self.local_user) self.assertEqual(rel.user_object, self.remote_user) + + + def test_follow_request_activity(self): + ''' accept a request and make it a relationship ''' + def mock_broadcast(_, activity, user): + self.assertEqual(user.remote_id, self.local_user.remote_id) + self.assertEqual(activity['actor'], self.local_user.remote_id) + self.assertEqual(activity['object'], self.remote_user.remote_id) + self.assertEqual(activity['type'], 'Follow') + + models.UserFollowRequest.broadcast = mock_broadcast + models.UserFollowRequest.objects.create( + user_subject=self.local_user, + user_object=self.remote_user, + ) + + + def test_follow_request_accept(self): + ''' accept a request and make it a relationship ''' + def mock_broadcast(_, activity, user): + self.assertEqual(user.remote_id, self.local_user.remote_id) + self.assertEqual(activity['type'], 'Accept') + self.assertEqual(activity['actor'], self.local_user.remote_id) + self.assertEqual( + activity['object']['id'], request.remote_id) + + models.UserFollowRequest.broadcast = mock_broadcast + request = models.UserFollowRequest.objects.create( + user_subject=self.remote_user, + user_object=self.local_user, + ) + request.accept() + + self.assertFalse(models.UserFollowRequest.objects.exists()) + self.assertTrue(models.UserFollows.objects.exists()) + rel = models.UserFollows.objects.get() + self.assertEqual(rel.user_subject, self.remote_user) + self.assertEqual(rel.user_object, self.local_user) + + + def test_follow_request_reject(self): + ''' accept a request and make it a relationship ''' + def mock_reject(_, activity, user): + self.assertEqual(user.remote_id, self.local_user.remote_id) + self.assertEqual(activity['type'], 'Reject') + self.assertEqual(activity['actor'], self.local_user.remote_id) + self.assertEqual( + activity['object']['id'], request.remote_id) + + models.UserFollowRequest.broadcast = mock_reject + request = models.UserFollowRequest.objects.create( + user_subject=self.remote_user, + user_object=self.local_user, + ) + request.reject() + + self.assertFalse(models.UserFollowRequest.objects.exists()) + self.assertFalse(models.UserFollows.objects.exists()) diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 196e84c0a..b72c50134 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -8,7 +8,7 @@ from .error import not_found_page, server_error_page from .federation import Federation from .feed import DirectMessage, Feed, Replies, Status from .follow import follow, unfollow -from .follow import accept_follow_request, delete_follow_request, handle_accept +from .follow import accept_follow_request, delete_follow_request from .goal import Goal from .import_data import Import, ImportStatus from .interaction import Favorite, Unfavorite, Boost, Unboost diff --git a/bookwyrm/views/follow.py b/bookwyrm/views/follow.py index 992b0dc10..f5b2ddd58 100644 --- a/bookwyrm/views/follow.py +++ b/bookwyrm/views/follow.py @@ -1,5 +1,4 @@ ''' views for actions you can take in the application ''' -from django.db import transaction from django.contrib.auth.decorators import login_required from django.http import HttpResponseBadRequest from django.shortcuts import redirect @@ -62,19 +61,11 @@ def accept_follow_request(request): except models.UserFollowRequest.DoesNotExist: # Request already dealt with. return redirect(request.user.local_path) - handle_accept(follow_request) + follow_request.accept() return redirect(request.user.local_path) -def handle_accept(follow_request): - ''' send an acceptance message to a follow request ''' - with transaction.atomic(): - relationship = models.UserFollows.from_request(follow_request) - follow_request.delete() - relationship.save() - - @login_required @require_POST def delete_follow_request(request):