broadcasting for follow, accept, and reject

This commit is contained in:
Mouse Reeve 2021-02-06 19:12:49 -08:00
parent ffd0759f6f
commit b02a2c1aa4
6 changed files with 140 additions and 101 deletions

View file

@ -9,7 +9,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
import requests import requests
from bookwyrm import activitypub, models, views from bookwyrm import activitypub, models
from bookwyrm import status as status_builder from bookwyrm import status as status_builder
from bookwyrm.tasks import app from bookwyrm.tasks import app
from bookwyrm.signatures import Signature from bookwyrm.signatures import Signature
@ -144,7 +144,7 @@ def handle_follow(activity):
related_user=relationship.user_subject related_user=relationship.user_subject
) )
if not manually_approves: if not manually_approves:
views.handle_accept(relationship) relationship.accept()
@app.task @app.task

View file

@ -366,12 +366,14 @@ class ActivityMixin(ActivitypubMixin):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
''' broadcast activity ''' ''' broadcast activity '''
super().save(*args, **kwargs) 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): def delete(self, *args, **kwargs):
''' nevermind, undo that activity ''' ''' 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) super().delete(*args, **kwargs)

View file

@ -1,15 +1,15 @@
''' defines relationships between users ''' ''' defines relationships between users '''
from django.db import models from django.db import models, transaction
from django.db.models import Q from django.db.models import Q
from django.dispatch import receiver from django.dispatch import receiver
from bookwyrm import activitypub from bookwyrm import activitypub
from .activitypub_mixin import ActivityMixin from .activitypub_mixin import ActivitypubMixin, ActivityMixin
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
from . import fields from . import fields
class UserRelationship(ActivityMixin, BookWyrmModel): class UserRelationship(BookWyrmModel):
''' many-to-many through table for followers ''' ''' many-to-many through table for followers '''
user_subject = fields.ForeignKey( user_subject = fields.ForeignKey(
'User', 'User',
@ -24,6 +24,11 @@ class UserRelationship(ActivityMixin, BookWyrmModel):
activitypub_field='object', activitypub_field='object',
) )
@property
def privacy(self):
''' all relationships are handled directly with the participants '''
return 'direct'
class Meta: class Meta:
''' relationships should be unique ''' ''' relationships should be unique '''
abstract = True 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 def get_remote_id(self, status=None):# pylint: disable=arguments-differ
''' use shelf identifier in remote_id ''' ''' use shelf identifier in remote_id '''
status = status or 'follows' status = status or 'follows'
@ -47,55 +50,73 @@ class UserRelationship(ActivityMixin, BookWyrmModel):
return '%s#%s/%d' % (base_path, status, self.id) return '%s#%s/%d' % (base_path, status, self.id)
def to_accept_activity(self): class UserFollows(ActivitypubMixin, UserRelationship):
''' 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):
''' Following a user ''' ''' Following a user '''
status = 'follows' status = 'follows'
activity_serializer = activitypub.Follow
@classmethod @classmethod
def from_request(cls, follow_request): def from_request(cls, follow_request):
''' converts a follow request into a follow relationship ''' ''' converts a follow request into a follow relationship '''
return cls( return cls.objects.create(
user_subject=follow_request.user_subject, user_subject=follow_request.user_subject,
user_object=follow_request.user_object, user_object=follow_request.user_object,
remote_id=follow_request.remote_id, remote_id=follow_request.remote_id,
) )
class UserFollowRequest(UserRelationship): class UserFollowRequest(ActivitypubMixin, UserRelationship):
''' following a user requires manual or automatic confirmation ''' ''' following a user requires manual or automatic confirmation '''
status = 'follow_request' status = 'follow_request'
activity_serializer = activitypub.Follow
def save(self, *args, **kwargs): 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: try:
UserFollows.objects.get( UserFollows.objects.get(
user_subject=self.user_subject, user_subject=self.user_subject,
user_object=self.user_object user_object=self.user_object
) )
UserBlocks.objects.get(
user_subject=self.user_subject,
user_object=self.user_object
)
return None return None
except UserFollows.DoesNotExist: except (UserFollows.DoesNotExist, UserBlocks.DoesNotExist):
return super().save(*args, **kwargs) 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 ''' ''' prevent another user from following you and seeing your posts '''
status = 'blocks' status = 'blocks'
activity_serializer = activitypub.Block activity_serializer = activitypub.Block

View file

@ -6,7 +6,9 @@ from bookwyrm import models
class Relationship(TestCase): class Relationship(TestCase):
''' following, blocking, stuff like that '''
def setUp(self): def setUp(self):
''' we need some users for this '''
with patch('bookwyrm.models.user.set_remote_server.delay'): with patch('bookwyrm.models.user.set_remote_server.delay'):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword', 'rat', 'rat@rat.com', 'ratword',
@ -22,65 +24,27 @@ class Relationship(TestCase):
self.local_user.save(broadcast=False) self.local_user.save(broadcast=False)
def test_user_follows(self): def test_user_follows(self):
rel = models.UserFollows.objects.create( ''' create a follow relationship '''
user_subject=self.local_user, with patch('bookwyrm.models.activitypub_mixin.ActivityMixin.broadcast'):
user_object=self.remote_user 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
)
activity = rel.to_activity() activity = rel.to_activity()
self.assertEqual(activity['id'], rel.remote_id) self.assertEqual(activity['id'], rel.remote_id)
self.assertEqual(activity['actor'], 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['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): 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( request = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_subject=self.local_user,
user_object=self.remote_user user_object=self.remote_user
@ -102,11 +66,14 @@ class Relationship(TestCase):
def test_user_follows_from_request_custom_remote_id(self): def test_user_follows_from_request_custom_remote_id(self):
request = models.UserFollowRequest.objects.create( ''' store a specific remote id for a relationship provided by remote '''
user_subject=self.local_user, with patch(
user_object=self.remote_user, 'bookwyrm.models.activitypub_mixin.ActivitypubMixin.broadcast'):
remote_id='http://antoher.server/sdkfhskdjf/23' request = models.UserFollowRequest.objects.create(
) user_subject=self.local_user,
user_object=self.remote_user,
remote_id='http://antoher.server/sdkfhskdjf/23'
)
self.assertEqual( self.assertEqual(
request.remote_id, request.remote_id,
'http://antoher.server/sdkfhskdjf/23' 'http://antoher.server/sdkfhskdjf/23'
@ -121,3 +88,61 @@ class Relationship(TestCase):
self.assertEqual(rel.status, 'follows') self.assertEqual(rel.status, 'follows')
self.assertEqual(rel.user_subject, self.local_user) self.assertEqual(rel.user_subject, self.local_user)
self.assertEqual(rel.user_object, self.remote_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())

View file

@ -8,7 +8,7 @@ from .error import not_found_page, server_error_page
from .federation import Federation from .federation import Federation
from .feed import DirectMessage, Feed, Replies, Status from .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow 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 .goal import Goal
from .import_data import Import, ImportStatus from .import_data import Import, ImportStatus
from .interaction import Favorite, Unfavorite, Boost, Unboost from .interaction import Favorite, Unfavorite, Boost, Unboost

View file

@ -1,5 +1,4 @@
''' views for actions you can take in the application ''' ''' views for actions you can take in the application '''
from django.db import transaction
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
from django.shortcuts import redirect from django.shortcuts import redirect
@ -62,19 +61,11 @@ def accept_follow_request(request):
except models.UserFollowRequest.DoesNotExist: except models.UserFollowRequest.DoesNotExist:
# Request already dealt with. # Request already dealt with.
return redirect(request.user.local_path) return redirect(request.user.local_path)
handle_accept(follow_request) follow_request.accept()
return redirect(request.user.local_path) 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 @login_required
@require_POST @require_POST
def delete_follow_request(request): def delete_follow_request(request):