diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index a49514dc7..631ca2dd7 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -50,6 +50,7 @@ def naive_parse(activity_objects, activity_json): return serializer(activity_objects=activity_objects, **activity_json) + @dataclass(init=False) class ActivityObject: ''' actor activitypub json ''' @@ -79,7 +80,7 @@ class ActivityObject: setattr(self, field.name, value) - def to_model(self, instance=None, save=True): + def to_model(self, instance=None, allow_create=True, save=True): ''' convert from an activity to a model instance ''' # figure out the right model -- wish I had a better way for this models = apps.get_models() @@ -95,7 +96,10 @@ class ActivityObject: return instance # check for an existing instance, if we're not updating a known obj - instance = instance or model.find_existing(self.serialize()) or model() + instance = instance or model.find_existing(self.serialize()) + if not instance and not allow_create: + return None + instance = instance or model() for field in instance.simple_fields: field.set_field_from_activity(instance, self) diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index fee5f3624..d781993ec 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -12,6 +12,10 @@ class Verb(ActivityObject): actor: str object: ActivityObject + def action(self): + ''' usually we just want to save, this can be overridden as needed ''' + self.object.to_model() + @dataclass(init=False) class Create(Verb): @@ -21,10 +25,6 @@ class Create(Verb): signature: Signature = None type: str = 'Create' - def action(self): - ''' create the model instance from the dataclass ''' - self.object.to_model() - @dataclass(init=False) class Delete(Verb): @@ -33,6 +33,12 @@ class Delete(Verb): cc: List type: str = 'Delete' + def action(self): + ''' find and delete the activity object ''' + obj = self.object.to_model(save=False, allow_create=False) + obj.delete() + + @dataclass(init=False) class Update(Verb): @@ -40,12 +46,21 @@ class Update(Verb): to: List type: str = 'Update' + def action(self): + ''' update a model instance from the dataclass ''' + self.object.to_model(allow_create=False) + @dataclass(init=False) class Undo(Verb): ''' Undo an activity ''' type: str = 'Undo' + def action(self): + ''' find and remove the activity object ''' + obj = self.object.to_model(save=False, allow_create=False) + obj.delete() + @dataclass(init=False) class Follow(Verb): @@ -53,18 +68,25 @@ class Follow(Verb): object: str type: str = 'Follow' + @dataclass(init=False) class Block(Verb): ''' Block activity ''' object: str type: str = 'Block' + @dataclass(init=False) class Accept(Verb): ''' Accept activity ''' object: Follow type: str = 'Accept' + def action(self): + ''' find and remove the activity object ''' + obj = self.object.to_model(save=False, allow_create=False) + obj.accept() + @dataclass(init=False) class Reject(Verb): @@ -72,6 +94,11 @@ class Reject(Verb): object: Follow type: str = 'Reject' + def action(self): + ''' find and remove the activity object ''' + obj = self.object.to_model(save=False, allow_create=False) + obj.reject() + @dataclass(init=False) class Add(Verb): @@ -101,3 +128,8 @@ class Remove(Verb): '''Remove activity ''' target: ActivityObject type: str = 'Remove' + + def action(self): + ''' find and remove the activity object ''' + obj = self.object.to_model(save=False, allow_create=False) + obj.delete() diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index e2db5468d..e4c6f4fa1 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -56,11 +56,9 @@ class UserRelationship(BookWyrmModel): return '%s#%s/%d' % (base_path, status, self.id) -class UserFollows(ActivitypubMixin, UserRelationship): +class UserFollows(UserRelationship): ''' Following a user ''' status = 'follows' - activity_serializer = activitypub.Follow - @classmethod def from_request(cls, follow_request): @@ -101,9 +99,13 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): self.broadcast(self.to_activity(), self.user_subject) if self.user_object.local: + manually_approves = self.user_object.manually_approves_followers + if manually_approves: + self.accept() + model = apps.get_model('bookwyrm.Notification', require_ready=True) - notification_type = 'FOLLOW_REQUEST' \ - if self.user_object.manually_approves_followers else 'FOLLOW' + notification_type = 'FOLLOW_REQUEST' if \ + manually_approves else 'FOLLOW' model.objects.create( user=self.user_object, related_user=self.user_subject, diff --git a/bookwyrm/tests/test_incoming.py b/bookwyrm/tests/test_incoming.py deleted file mode 100644 index 288ae3020..000000000 --- a/bookwyrm/tests/test_incoming.py +++ /dev/null @@ -1,493 +0,0 @@ -''' test incoming activities ''' -from datetime import datetime -import json -import pathlib -from unittest.mock import patch - -from django.http import HttpResponseBadRequest, HttpResponseNotAllowed, \ - HttpResponseNotFound -from django.test import TestCase -from django.test.client import RequestFactory -import responses - -from bookwyrm import models, incoming - - -#pylint: disable=too-many-public-methods -class Incoming(TestCase): - ''' a lot here: all handlers for receiving activitypub requests ''' - def setUp(self): - ''' we need basic things, like users ''' - self.local_user = models.User.objects.create_user( - 'mouse@example.com', 'mouse@mouse.com', 'mouseword', - local=True, localname='mouse') - self.local_user.remote_id = 'https://example.com/user/mouse' - self.local_user.save(broadcast=False) - with patch('bookwyrm.models.user.set_remote_server.delay'): - self.remote_user = models.User.objects.create_user( - 'rat', 'rat@rat.com', 'ratword', - local=False, - remote_id='https://example.com/users/rat', - inbox='https://example.com/users/rat/inbox', - outbox='https://example.com/users/rat/outbox', - ) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - self.status = models.Status.objects.create( - user=self.local_user, - content='Test status', - remote_id='https://example.com/status/1', - ) - self.factory = RequestFactory() - - - - def test_handle_follow(self): - ''' remote user wants to follow local user ''' - activity = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/users/rat/follows/123", - "type": "Follow", - "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse" - } - - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - incoming.handle_follow(activity) - - # notification created - notification = models.Notification.objects.get() - self.assertEqual(notification.user, self.local_user) - self.assertEqual(notification.notification_type, 'FOLLOW') - - # the request should have been deleted - requests = models.UserFollowRequest.objects.all() - self.assertEqual(list(requests), []) - - # the follow relationship should exist - follow = models.UserFollows.objects.get(user_object=self.local_user) - self.assertEqual(follow.user_subject, self.remote_user) - - - def test_handle_follow_manually_approved(self): - ''' needs approval before following ''' - activity = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/users/rat/follows/123", - "type": "Follow", - "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse" - } - - self.local_user.manually_approves_followers = True - self.local_user.save(broadcast=False) - - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - incoming.handle_follow(activity) - - # notification created - notification = models.Notification.objects.get() - self.assertEqual(notification.user, self.local_user) - self.assertEqual(notification.notification_type, 'FOLLOW_REQUEST') - - # the request should exist - request = models.UserFollowRequest.objects.get() - self.assertEqual(request.user_subject, self.remote_user) - self.assertEqual(request.user_object, self.local_user) - - # the follow relationship should not exist - follow = models.UserFollows.objects.all() - self.assertEqual(list(follow), []) - - - def test_handle_unfollow(self): - ''' remove a relationship ''' - activity = { - "type": "Undo", - "@context": "https://www.w3.org/ns/activitystreams", - "object": { - "id": "https://example.com/users/rat/follows/123", - "type": "Follow", - "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse" - } - } - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - models.UserFollows.objects.create( - user_subject=self.remote_user, user_object=self.local_user) - self.assertEqual(self.remote_user, self.local_user.followers.first()) - - incoming.handle_unfollow(activity) - self.assertIsNone(self.local_user.followers.first()) - - - def test_handle_follow_accept(self): - ''' a remote user approved a follow request from local ''' - activity = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/users/rat/follows/123#accepts", - "type": "Accept", - "actor": "https://example.com/users/rat", - "object": { - "id": "https://example.com/users/rat/follows/123", - "type": "Follow", - "actor": "https://example.com/user/mouse", - "object": "https://example.com/users/rat" - } - } - - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - models.UserFollowRequest.objects.create( - user_subject=self.local_user, - user_object=self.remote_user - ) - self.assertEqual(models.UserFollowRequest.objects.count(), 1) - - incoming.handle_follow_accept(activity) - - # request should be deleted - self.assertEqual(models.UserFollowRequest.objects.count(), 0) - - # relationship should be created - follows = self.remote_user.followers - self.assertEqual(follows.count(), 1) - self.assertEqual(follows.first(), self.local_user) - - - def test_handle_follow_reject(self): - ''' turn down a follow request ''' - activity = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/users/rat/follows/123#accepts", - "type": "Reject", - "actor": "https://example.com/users/rat", - "object": { - "id": "https://example.com/users/rat/follows/123", - "type": "Follow", - "actor": "https://example.com/user/mouse", - "object": "https://example.com/users/rat" - } - } - - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - models.UserFollowRequest.objects.create( - user_subject=self.local_user, - user_object=self.remote_user - ) - self.assertEqual(models.UserFollowRequest.objects.count(), 1) - - incoming.handle_follow_reject(activity) - - # request should be deleted - self.assertEqual(models.UserFollowRequest.objects.count(), 0) - - # relationship should be created - follows = self.remote_user.followers - self.assertEqual(follows.count(), 0) - - - def test_handle_update_list(self): - ''' a new list ''' - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - book_list = models.List.objects.create( - name='hi', remote_id='https://example.com/list/22', - user=self.local_user) - activity = { - 'object': { - "id": "https://example.com/list/22", - "type": "BookList", - "totalItems": 1, - "first": "https://example.com/list/22?page=1", - "last": "https://example.com/list/22?page=1", - "name": "Test List", - "owner": "https://example.com/user/mouse", - "to": [ - "https://www.w3.org/ns/activitystreams#Public" - ], - "cc": [ - "https://example.com/user/mouse/followers" - ], - "summary": "summary text", - "curation": "curated", - "@context": "https://www.w3.org/ns/activitystreams" - } - } - incoming.handle_update_list(activity) - book_list.refresh_from_db() - self.assertEqual(book_list.name, 'Test List') - self.assertEqual(book_list.curation, 'curated') - self.assertEqual(book_list.description, 'summary text') - self.assertEqual(book_list.remote_id, 'https://example.com/list/22') - - - def test_handle_delete_status(self): - ''' remove a status ''' - self.status.user = self.remote_user - self.status.save(broadcast=False) - - self.assertFalse(self.status.deleted) - activity = { - 'type': 'Delete', - 'id': '%s/activity' % self.status.remote_id, - 'actor': self.remote_user.remote_id, - 'object': {'id': self.status.remote_id}, - } - incoming.handle_delete_status(activity) - # deletion doens't remove the status, it turns it into a tombstone - status = models.Status.objects.get() - self.assertTrue(status.deleted) - self.assertIsInstance(status.deleted_date, datetime) - - - def test_handle_delete_status_notifications(self): - ''' remove a status with related notifications ''' - self.status.user = self.remote_user - self.status.save(broadcast=False) - models.Notification.objects.create( - related_status=self.status, - user=self.local_user, - notification_type='MENTION' - ) - # this one is innocent, don't delete it - notif = models.Notification.objects.create( - user=self.local_user, - notification_type='MENTION' - ) - self.assertFalse(self.status.deleted) - self.assertEqual(models.Notification.objects.count(), 2) - activity = { - 'type': 'Delete', - 'id': '%s/activity' % self.status.remote_id, - 'actor': self.remote_user.remote_id, - 'object': {'id': self.status.remote_id}, - } - incoming.handle_delete_status(activity) - # deletion doens't remove the status, it turns it into a tombstone - status = models.Status.objects.get() - self.assertTrue(status.deleted) - self.assertIsInstance(status.deleted_date, datetime) - - # notifications should be truly deleted - self.assertEqual(models.Notification.objects.count(), 1) - self.assertEqual(models.Notification.objects.get(), notif) - - - def test_handle_favorite(self): - ''' fav a status ''' - activity = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': 'https://example.com/fav/1', - 'actor': 'https://example.com/users/rat', - 'published': 'Mon, 25 May 2020 19:31:20 GMT', - 'object': 'https://example.com/status/1', - } - - incoming.handle_favorite(activity) - - fav = models.Favorite.objects.get(remote_id='https://example.com/fav/1') - self.assertEqual(fav.status, self.status) - self.assertEqual(fav.remote_id, 'https://example.com/fav/1') - self.assertEqual(fav.user, self.remote_user) - - def test_handle_unfavorite(self): - ''' fav a status ''' - activity = { - 'id': 'https://example.com/fav/1#undo', - 'type': 'Undo', - 'object': { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': 'https://example.com/fav/1', - 'actor': 'https://example.com/users/rat', - 'published': 'Mon, 25 May 2020 19:31:20 GMT', - 'object': 'https://example.com/fav/1', - } - } - models.Favorite.objects.create( - status=self.status, - user=self.remote_user, - remote_id='https://example.com/fav/1') - self.assertEqual(models.Favorite.objects.count(), 1) - - incoming.handle_unfavorite(activity) - self.assertEqual(models.Favorite.objects.count(), 0) - - - def test_handle_boost(self): - ''' boost a status ''' - self.assertEqual(models.Notification.objects.count(), 0) - activity = { - 'type': 'Announce', - 'id': '%s/boost' % self.status.remote_id, - 'actor': self.remote_user.remote_id, - 'object': self.status.to_activity(), - } - with patch('bookwyrm.models.status.Status.ignore_activity') \ - as discarder: - discarder.return_value = False - incoming.handle_boost(activity) - boost = models.Boost.objects.get() - self.assertEqual(boost.boosted_status, self.status) - notification = models.Notification.objects.get() - self.assertEqual(notification.user, self.local_user) - self.assertEqual(notification.related_status, self.status) - - - @responses.activate - def test_handle_discarded_boost(self): - ''' test a boost of a mastodon status that will be discarded ''' - activity = { - 'type': 'Announce', - 'id': 'http://www.faraway.com/boost/12', - 'actor': self.remote_user.remote_id, - 'object': self.status.to_activity(), - } - responses.add( - responses.GET, - 'http://www.faraway.com/boost/12', - json={'id': 'http://www.faraway.com/boost/12'}, - status=200) - incoming.handle_boost(activity) - self.assertEqual(models.Boost.objects.count(), 0) - - - def test_handle_unboost(self): - ''' undo a boost ''' - activity = { - 'type': 'Undo', - 'object': { - 'type': 'Announce', - 'id': '%s/boost' % self.status.remote_id, - 'actor': self.local_user.remote_id, - 'object': self.status.to_activity(), - } - } - models.Boost.objects.create( - boosted_status=self.status, user=self.remote_user) - incoming.handle_unboost(activity) - - - def test_handle_add_book(self): - ''' shelving a book ''' - book = models.Edition.objects.create( - title='Test', remote_id='https://bookwyrm.social/book/37292') - shelf = models.Shelf.objects.create( - user=self.remote_user, name='Test Shelf') - shelf.remote_id = 'https://bookwyrm.social/user/mouse/shelf/to-read' - shelf.save() - - activity = { - "id": "https://bookwyrm.social/shelfbook/6189#add", - "type": "Add", - "actor": "https://example.com/users/rat", - "object": "https://bookwyrm.social/book/37292", - "target": "https://bookwyrm.social/user/mouse/shelf/to-read", - "@context": "https://www.w3.org/ns/activitystreams" - } - incoming.handle_add(activity) - self.assertEqual(shelf.books.first(), book) - - - def test_handle_update_user(self): - ''' update an existing user ''' - # we only do this with remote users - self.local_user.local = False - self.local_user.save() - - datafile = pathlib.Path(__file__).parent.joinpath( - 'data/ap_user.json') - userdata = json.loads(datafile.read_bytes()) - del userdata['icon'] - self.assertIsNone(self.local_user.name) - incoming.handle_update_user({'object': userdata}) - user = models.User.objects.get(id=self.local_user.id) - self.assertEqual(user.name, 'MOUSE?? MOUSE!!') - self.assertEqual(user.username, 'mouse@example.com') - self.assertEqual(user.localname, 'mouse') - - - def test_handle_update_edition(self): - ''' update an existing edition ''' - datafile = pathlib.Path(__file__).parent.joinpath( - 'data/bw_edition.json') - bookdata = json.loads(datafile.read_bytes()) - - models.Work.objects.create( - title='Test Work', remote_id='https://bookwyrm.social/book/5988') - book = models.Edition.objects.create( - title='Test Book', remote_id='https://bookwyrm.social/book/5989') - - del bookdata['authors'] - self.assertEqual(book.title, 'Test Book') - - with patch( - 'bookwyrm.activitypub.base_activity.set_related_field.delay'): - incoming.handle_update_edition({'object': bookdata}) - book = models.Edition.objects.get(id=book.id) - self.assertEqual(book.title, 'Piranesi') - - - def test_handle_update_work(self): - ''' update an existing edition ''' - datafile = pathlib.Path(__file__).parent.joinpath( - 'data/bw_work.json') - bookdata = json.loads(datafile.read_bytes()) - - book = models.Work.objects.create( - title='Test Book', remote_id='https://bookwyrm.social/book/5988') - - del bookdata['authors'] - self.assertEqual(book.title, 'Test Book') - with patch( - 'bookwyrm.activitypub.base_activity.set_related_field.delay'): - incoming.handle_update_work({'object': bookdata}) - book = models.Work.objects.get(id=book.id) - self.assertEqual(book.title, 'Piranesi') - - - def test_handle_blocks(self): - ''' create a "block" database entry from an activity ''' - self.local_user.followers.add(self.remote_user) - with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): - models.UserFollowRequest.objects.create( - user_subject=self.local_user, - user_object=self.remote_user) - self.assertTrue(models.UserFollows.objects.exists()) - self.assertTrue(models.UserFollowRequest.objects.exists()) - - activity = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/9e1f41ac-9ddd-4159", - "type": "Block", - "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse" - } - - incoming.handle_block(activity) - block = models.UserBlocks.objects.get() - self.assertEqual(block.user_subject, self.remote_user) - self.assertEqual(block.user_object, self.local_user) - self.assertEqual( - block.remote_id, 'https://example.com/9e1f41ac-9ddd-4159') - - self.assertFalse(models.UserFollows.objects.exists()) - self.assertFalse(models.UserFollowRequest.objects.exists()) - - - def test_handle_unblock(self): - ''' unblock a user ''' - self.remote_user.blocks.add(self.local_user) - - block = models.UserBlocks.objects.get() - block.remote_id = 'https://example.com/9e1f41ac-9ddd-4159' - block.save() - - self.assertEqual(block.user_subject, self.remote_user) - self.assertEqual(block.user_object, self.local_user) - activity = {'type': 'Undo', 'object': { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/9e1f41ac-9ddd-4159", - "type": "Block", - "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse" - }} - incoming.handle_unblock(activity) - self.assertFalse(models.UserBlocks.objects.exists()) diff --git a/bookwyrm/tests/views/test_follow.py b/bookwyrm/tests/views/test_follow.py index 943ffcf82..9a1576599 100644 --- a/bookwyrm/tests/views/test_follow.py +++ b/bookwyrm/tests/views/test_follow.py @@ -105,6 +105,8 @@ class BookViews(TestCase): request.user = self.local_user self.remote_user.followers.add(self.local_user) self.assertEqual(self.remote_user.followers.count(), 1) + # need to see if this ACTUALLY broadcasts + raise ValueError() with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): views.unfollow(request) diff --git a/bookwyrm/tests/views/test_inbox.py b/bookwyrm/tests/views/test_inbox.py index 784f2127b..9ca7198e4 100644 --- a/bookwyrm/tests/views/test_inbox.py +++ b/bookwyrm/tests/views/test_inbox.py @@ -1,13 +1,17 @@ ''' tests incoming activities''' +from datetime import datetime import json import pathlib from unittest.mock import patch from django.http import HttpResponseNotAllowed, HttpResponseNotFound from django.test import TestCase, Client +import responses from bookwyrm import models, views + +#pylint: disable=too-many-public-methods class Inbox(TestCase): ''' readthrough tests ''' def setUp(self): @@ -230,3 +234,455 @@ class Inbox(TestCase): self.assertEqual(book_list.curation, 'curated') self.assertEqual(book_list.description, 'summary text') self.assertEqual(book_list.remote_id, 'https://example.com/list/22') + + + def test_handle_follow(self): + ''' remote user wants to follow local user ''' + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/rat/follows/123", + "type": "Follow", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse" + } + + with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + views.inbox.activity_task(activity) + + # notification created + notification = models.Notification.objects.get() + self.assertEqual(notification.user, self.local_user) + self.assertEqual(notification.notification_type, 'FOLLOW') + + # the request should have been deleted + requests = models.UserFollowRequest.objects.all() + self.assertEqual(list(requests), []) + + # the follow relationship should exist + follow = models.UserFollows.objects.get(user_object=self.local_user) + self.assertEqual(follow.user_subject, self.remote_user) + + + def test_handle_follow_manually_approved(self): + ''' needs approval before following ''' + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/rat/follows/123", + "type": "Follow", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse" + } + + self.local_user.manually_approves_followers = True + self.local_user.save(broadcast=False) + + with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + views.inbox.activity_task(activity) + + # notification created + notification = models.Notification.objects.get() + self.assertEqual(notification.user, self.local_user) + self.assertEqual(notification.notification_type, 'FOLLOW_REQUEST') + + # the request should exist + request = models.UserFollowRequest.objects.get() + self.assertEqual(request.user_subject, self.remote_user) + self.assertEqual(request.user_object, self.local_user) + + # the follow relationship should not exist + follow = models.UserFollows.objects.all() + self.assertEqual(list(follow), []) + + + def test_handle_unfollow(self): + ''' remove a relationship ''' + activity = { + "type": "Undo", + "@context": "https://www.w3.org/ns/activitystreams", + "object": { + "id": "https://example.com/users/rat/follows/123", + "type": "Follow", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse" + } + } + with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + models.UserFollows.objects.create( + user_subject=self.remote_user, user_object=self.local_user) + self.assertEqual(self.remote_user, self.local_user.followers.first()) + + views.inbox.activity_task(activity) + self.assertIsNone(self.local_user.followers.first()) + + + def test_handle_follow_accept(self): + ''' a remote user approved a follow request from local ''' + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/rat/follows/123#accepts", + "type": "Accept", + "actor": "https://example.com/users/rat", + "object": { + "id": "https://example.com/users/rat/follows/123", + "type": "Follow", + "actor": "https://example.com/user/mouse", + "object": "https://example.com/users/rat" + } + } + + with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + models.UserFollowRequest.objects.create( + user_subject=self.local_user, + user_object=self.remote_user + ) + self.assertEqual(models.UserFollowRequest.objects.count(), 1) + + views.inbox.activity_task(activity) + + # request should be deleted + self.assertEqual(models.UserFollowRequest.objects.count(), 0) + + # relationship should be created + follows = self.remote_user.followers + self.assertEqual(follows.count(), 1) + self.assertEqual(follows.first(), self.local_user) + + + def test_handle_follow_reject(self): + ''' turn down a follow request ''' + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/rat/follows/123#accepts", + "type": "Reject", + "actor": "https://example.com/users/rat", + "object": { + "id": "https://example.com/users/rat/follows/123", + "type": "Follow", + "actor": "https://example.com/user/mouse", + "object": "https://example.com/users/rat" + } + } + + with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + models.UserFollowRequest.objects.create( + user_subject=self.local_user, + user_object=self.remote_user + ) + self.assertEqual(models.UserFollowRequest.objects.count(), 1) + + views.inbox.activity_task(activity) + + # request should be deleted + self.assertEqual(models.UserFollowRequest.objects.count(), 0) + + # relationship should be created + follows = self.remote_user.followers + self.assertEqual(follows.count(), 0) + + + def test_handle_update_list(self): + ''' a new list ''' + with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + book_list = models.List.objects.create( + name='hi', remote_id='https://example.com/list/22', + user=self.local_user) + activity = { + 'object': { + "id": "https://example.com/list/22", + "type": "BookList", + "totalItems": 1, + "first": "https://example.com/list/22?page=1", + "last": "https://example.com/list/22?page=1", + "name": "Test List", + "owner": "https://example.com/user/mouse", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://example.com/user/mouse/followers" + ], + "summary": "summary text", + "curation": "curated", + "@context": "https://www.w3.org/ns/activitystreams" + } + } + views.inbox.activity_task(activity) + book_list.refresh_from_db() + self.assertEqual(book_list.name, 'Test List') + self.assertEqual(book_list.curation, 'curated') + self.assertEqual(book_list.description, 'summary text') + self.assertEqual(book_list.remote_id, 'https://example.com/list/22') + + + def test_handle_delete_status(self): + ''' remove a status ''' + self.status.user = self.remote_user + self.status.save(broadcast=False) + + self.assertFalse(self.status.deleted) + activity = { + 'type': 'Delete', + 'id': '%s/activity' % self.status.remote_id, + 'actor': self.remote_user.remote_id, + 'object': {'id': self.status.remote_id}, + } + views.inbox.activity_task(activity) + # deletion doens't remove the status, it turns it into a tombstone + status = models.Status.objects.get() + self.assertTrue(status.deleted) + self.assertIsInstance(status.deleted_date, datetime) + + + def test_handle_delete_status_notifications(self): + ''' remove a status with related notifications ''' + self.status.user = self.remote_user + self.status.save(broadcast=False) + models.Notification.objects.create( + related_status=self.status, + user=self.local_user, + notification_type='MENTION' + ) + # this one is innocent, don't delete it + notif = models.Notification.objects.create( + user=self.local_user, + notification_type='MENTION' + ) + self.assertFalse(self.status.deleted) + self.assertEqual(models.Notification.objects.count(), 2) + activity = { + 'type': 'Delete', + 'id': '%s/activity' % self.status.remote_id, + 'actor': self.remote_user.remote_id, + 'object': {'id': self.status.remote_id}, + } + views.inbox.activity_task(activity) + # deletion doens't remove the status, it turns it into a tombstone + status = models.Status.objects.get() + self.assertTrue(status.deleted) + self.assertIsInstance(status.deleted_date, datetime) + + # notifications should be truly deleted + self.assertEqual(models.Notification.objects.count(), 1) + self.assertEqual(models.Notification.objects.get(), notif) + + + def test_handle_favorite(self): + ''' fav a status ''' + activity = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': 'https://example.com/fav/1', + 'actor': 'https://example.com/users/rat', + 'published': 'Mon, 25 May 2020 19:31:20 GMT', + 'object': 'https://example.com/status/1', + } + + views.inbox.activity_task(activity) + + fav = models.Favorite.objects.get(remote_id='https://example.com/fav/1') + self.assertEqual(fav.status, self.status) + self.assertEqual(fav.remote_id, 'https://example.com/fav/1') + self.assertEqual(fav.user, self.remote_user) + + def test_handle_unfavorite(self): + ''' fav a status ''' + activity = { + 'id': 'https://example.com/fav/1#undo', + 'type': 'Undo', + 'object': { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': 'https://example.com/fav/1', + 'actor': 'https://example.com/users/rat', + 'published': 'Mon, 25 May 2020 19:31:20 GMT', + 'object': 'https://example.com/fav/1', + } + } + models.Favorite.objects.create( + status=self.status, + user=self.remote_user, + remote_id='https://example.com/fav/1') + self.assertEqual(models.Favorite.objects.count(), 1) + + views.inbox.activity_task(activity) + self.assertEqual(models.Favorite.objects.count(), 0) + + + def test_handle_boost(self): + ''' boost a status ''' + self.assertEqual(models.Notification.objects.count(), 0) + activity = { + 'type': 'Announce', + 'id': '%s/boost' % self.status.remote_id, + 'actor': self.remote_user.remote_id, + 'object': self.status.to_activity(), + } + with patch('bookwyrm.models.status.Status.ignore_activity') \ + as discarder: + discarder.return_value = False + views.inbox.activity_task(activity) + boost = models.Boost.objects.get() + self.assertEqual(boost.boosted_status, self.status) + notification = models.Notification.objects.get() + self.assertEqual(notification.user, self.local_user) + self.assertEqual(notification.related_status, self.status) + + + @responses.activate + def test_handle_discarded_boost(self): + ''' test a boost of a mastodon status that will be discarded ''' + activity = { + 'type': 'Announce', + 'id': 'http://www.faraway.com/boost/12', + 'actor': self.remote_user.remote_id, + 'object': self.status.to_activity(), + } + responses.add( + responses.GET, + 'http://www.faraway.com/boost/12', + json={'id': 'http://www.faraway.com/boost/12'}, + status=200) + views.inbox.activity_task(activity) + self.assertEqual(models.Boost.objects.count(), 0) + + + def test_handle_unboost(self): + ''' undo a boost ''' + activity = { + 'type': 'Undo', + 'object': { + 'type': 'Announce', + 'id': '%s/boost' % self.status.remote_id, + 'actor': self.local_user.remote_id, + 'object': self.status.to_activity(), + } + } + models.Boost.objects.create( + boosted_status=self.status, user=self.remote_user) + views.inbox.activity_task(activity) + + + def test_handle_add_book(self): + ''' shelving a book ''' + book = models.Edition.objects.create( + title='Test', remote_id='https://bookwyrm.social/book/37292') + shelf = models.Shelf.objects.create( + user=self.remote_user, name='Test Shelf') + shelf.remote_id = 'https://bookwyrm.social/user/mouse/shelf/to-read' + shelf.save() + + activity = { + "id": "https://bookwyrm.social/shelfbook/6189#add", + "type": "Add", + "actor": "https://example.com/users/rat", + "object": "https://bookwyrm.social/book/37292", + "target": "https://bookwyrm.social/user/mouse/shelf/to-read", + "@context": "https://www.w3.org/ns/activitystreams" + } + views.inbox.activity_task(activity) + self.assertEqual(shelf.books.first(), book) + + + def test_handle_update_user(self): + ''' update an existing user ''' + # we only do this with remote users + self.local_user.local = False + self.local_user.save() + + datafile = pathlib.Path(__file__).parent.joinpath( + 'data/ap_user.json') + userdata = json.loads(datafile.read_bytes()) + del userdata['icon'] + self.assertIsNone(self.local_user.name) + views.inbox.activity_task({'object': userdata}) + user = models.User.objects.get(id=self.local_user.id) + self.assertEqual(user.name, 'MOUSE?? MOUSE!!') + self.assertEqual(user.username, 'mouse@example.com') + self.assertEqual(user.localname, 'mouse') + + + def test_handle_update_edition(self): + ''' update an existing edition ''' + datafile = pathlib.Path(__file__).parent.joinpath( + 'data/bw_edition.json') + bookdata = json.loads(datafile.read_bytes()) + + models.Work.objects.create( + title='Test Work', remote_id='https://bookwyrm.social/book/5988') + book = models.Edition.objects.create( + title='Test Book', remote_id='https://bookwyrm.social/book/5989') + + del bookdata['authors'] + self.assertEqual(book.title, 'Test Book') + + with patch( + 'bookwyrm.activitypub.base_activity.set_related_field.delay'): + views.inbox.activity_task({'object': bookdata}) + book = models.Edition.objects.get(id=book.id) + self.assertEqual(book.title, 'Piranesi') + + + def test_handle_update_work(self): + ''' update an existing edition ''' + datafile = pathlib.Path(__file__).parent.joinpath( + 'data/bw_work.json') + bookdata = json.loads(datafile.read_bytes()) + + book = models.Work.objects.create( + title='Test Book', remote_id='https://bookwyrm.social/book/5988') + + del bookdata['authors'] + self.assertEqual(book.title, 'Test Book') + with patch( + 'bookwyrm.activitypub.base_activity.set_related_field.delay'): + views.inbox.activity_task({'object': bookdata}) + book = models.Work.objects.get(id=book.id) + self.assertEqual(book.title, 'Piranesi') + + + def test_handle_blocks(self): + ''' create a "block" database entry from an activity ''' + self.local_user.followers.add(self.remote_user) + with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + models.UserFollowRequest.objects.create( + user_subject=self.local_user, + user_object=self.remote_user) + self.assertTrue(models.UserFollows.objects.exists()) + self.assertTrue(models.UserFollowRequest.objects.exists()) + + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/9e1f41ac-9ddd-4159", + "type": "Block", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse" + } + + views.inbox.activity_task(activity) + block = models.UserBlocks.objects.get() + self.assertEqual(block.user_subject, self.remote_user) + self.assertEqual(block.user_object, self.local_user) + self.assertEqual( + block.remote_id, 'https://example.com/9e1f41ac-9ddd-4159') + + self.assertFalse(models.UserFollows.objects.exists()) + self.assertFalse(models.UserFollowRequest.objects.exists()) + + + def test_handle_unblock(self): + ''' unblock a user ''' + self.remote_user.blocks.add(self.local_user) + + block = models.UserBlocks.objects.get() + block.remote_id = 'https://example.com/9e1f41ac-9ddd-4159' + block.save() + + self.assertEqual(block.user_subject, self.remote_user) + self.assertEqual(block.user_object, self.local_user) + activity = {'type': 'Undo', 'object': { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/9e1f41ac-9ddd-4159", + "type": "Block", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse" + }} + views.inbox.activity_task(activity) + self.assertFalse(models.UserBlocks.objects.exists()) diff --git a/bookwyrm/views/follow.py b/bookwyrm/views/follow.py index c59f2e6d2..e95355e67 100644 --- a/bookwyrm/views/follow.py +++ b/bookwyrm/views/follow.py @@ -22,8 +22,6 @@ def follow(request): user_object=to_follow, ) - if to_follow.local and not to_follow.manually_approves_followers: - rel.accept() return redirect(to_follow.local_path)