mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-24 02:21:04 +00:00
broadcasting for follow, accept, and reject
This commit is contained in:
parent
ffd0759f6f
commit
b02a2c1aa4
6 changed files with 140 additions and 101 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
''' create a follow relationship '''
|
||||||
|
with patch('bookwyrm.models.activitypub_mixin.ActivityMixin.broadcast'):
|
||||||
rel = models.UserFollows.objects.create(
|
rel = models.UserFollows.objects.create(
|
||||||
user_subject=self.local_user,
|
user_subject=self.local_user,
|
||||||
user_object=self.remote_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,6 +66,9 @@ class Relationship(TestCase):
|
||||||
|
|
||||||
|
|
||||||
def test_user_follows_from_request_custom_remote_id(self):
|
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(
|
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,
|
||||||
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue