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
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

View file

@ -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)

View file

@ -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

View file

@ -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):
''' 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
)
self.assertEqual(
rel.remote_id,
'http://local.com/user/mouse#follows/%d' % rel.id
)
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,6 +66,9 @@ class Relationship(TestCase):
def test_user_follows_from_request_custom_remote_id(self):
''' 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,
@ -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())

View file

@ -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

View file

@ -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):