moviewyrm/bookwyrm/models/relationship.py

198 lines
6.7 KiB
Python
Raw Normal View History

2021-03-08 16:49:10 +00:00
""" defines relationships between users """
2021-02-11 00:06:50 +00:00
from django.apps import apps
2021-02-15 20:51:34 +00:00
from django.db import models, transaction, IntegrityError
2021-01-25 01:07:19 +00:00
from django.db.models import Q
2020-09-17 20:09:11 +00:00
from bookwyrm import activitypub
2021-02-17 22:37:20 +00:00
from .activitypub_mixin import ActivitypubMixin, ActivityMixin
from .activitypub_mixin import generate_activity
from .base_model import BookWyrmModel
from . import fields
2020-09-17 20:09:11 +00:00
class UserRelationship(BookWyrmModel):
2021-04-26 16:15:42 +00:00
"""many-to-many through table for followers"""
2021-03-08 16:49:10 +00:00
user_subject = fields.ForeignKey(
2021-03-08 16:49:10 +00:00
"User",
2020-09-17 20:09:11 +00:00
on_delete=models.PROTECT,
2021-03-08 16:49:10 +00:00
related_name="%(class)s_user_subject",
activitypub_field="actor",
2020-09-17 20:09:11 +00:00
)
user_object = fields.ForeignKey(
2021-03-08 16:49:10 +00:00
"User",
2020-09-17 20:09:11 +00:00
on_delete=models.PROTECT,
2021-03-08 16:49:10 +00:00
related_name="%(class)s_user_object",
activitypub_field="object",
2020-09-17 20:09:11 +00:00
)
@property
def privacy(self):
2021-04-26 16:15:42 +00:00
"""all relationships are handled directly with the participants"""
2021-03-08 16:49:10 +00:00
return "direct"
@property
def recipients(self):
2021-04-26 16:15:42 +00:00
"""the remote user needs to recieve direct broadcasts"""
return [u for u in [self.user_subject, self.user_object] if not u.local]
2020-09-17 20:09:11 +00:00
class Meta:
2021-04-26 16:15:42 +00:00
"""relationships should be unique"""
2021-03-08 16:49:10 +00:00
2020-09-17 20:09:11 +00:00
abstract = True
constraints = [
models.UniqueConstraint(
2021-03-08 16:49:10 +00:00
fields=["user_subject", "user_object"], name="%(class)s_unique"
2020-09-17 20:09:11 +00:00
),
models.CheckConstraint(
2021-03-08 16:49:10 +00:00
check=~models.Q(user_subject=models.F("user_object")),
name="%(class)s_no_self",
),
2020-09-17 20:09:11 +00:00
]
def get_remote_id(self):
2021-04-26 16:15:42 +00:00
"""use shelf identifier in remote_id"""
2020-09-17 20:09:11 +00:00
base_path = self.user_subject.remote_id
2021-09-18 18:32:00 +00:00
return f"{base_path}#follows/{self.id}"
2020-09-17 20:09:11 +00:00
2021-02-17 19:45:21 +00:00
class UserFollows(ActivityMixin, UserRelationship):
2021-04-26 16:15:42 +00:00
"""Following a user"""
2021-03-08 16:49:10 +00:00
status = "follows"
2020-09-17 20:09:11 +00:00
def to_activity(self): # pylint: disable=arguments-differ
2021-04-26 16:15:42 +00:00
"""overrides default to manually set serializer"""
2021-02-17 19:45:21 +00:00
return activitypub.Follow(**generate_activity(self))
2021-02-15 20:51:34 +00:00
def save(self, *args, **kwargs):
2021-04-26 16:15:42 +00:00
"""really really don't let a user follow someone who blocked them"""
2021-02-15 20:51:34 +00:00
# blocking in either direction is a no-go
if UserBlocks.objects.filter(
2021-03-08 16:49:10 +00:00
Q(
user_subject=self.user_subject,
user_object=self.user_object,
)
| Q(
user_subject=self.user_object,
user_object=self.user_subject,
)
).exists():
2021-02-15 20:51:34 +00:00
raise IntegrityError()
# don't broadcast this type of relationship -- accepts and requests
# are handled by the UserFollowRequest model
super().save(*args, broadcast=False, **kwargs)
2021-02-17 19:45:21 +00:00
2020-09-17 20:09:11 +00:00
@classmethod
def from_request(cls, follow_request):
2021-04-26 16:15:42 +00:00
"""converts a follow request into a follow relationship"""
return cls.objects.create(
2020-09-17 20:09:11 +00:00
user_subject=follow_request.user_subject,
user_object=follow_request.user_object,
remote_id=follow_request.remote_id,
2020-09-17 20:09:11 +00:00
)
class UserFollowRequest(ActivitypubMixin, UserRelationship):
2021-04-26 16:15:42 +00:00
"""following a user requires manual or automatic confirmation"""
2021-03-08 16:49:10 +00:00
status = "follow_request"
activity_serializer = activitypub.Follow
2020-09-17 20:09:11 +00:00
2021-06-18 21:29:24 +00:00
def save(self, *args, broadcast=True, **kwargs): # pylint: disable=arguments-differ
2021-04-26 16:15:42 +00:00
"""make sure the follow or block relationship doesn't already exist"""
# if there's a request for a follow that already exists, accept it
# without changing the local database state
2021-02-15 20:51:34 +00:00
if UserFollows.objects.filter(
2021-03-08 16:49:10 +00:00
user_subject=self.user_subject,
user_object=self.user_object,
).exists():
self.accept(broadcast_only=True)
return
2021-02-15 20:51:34 +00:00
# blocking in either direction is a no-go
if UserBlocks.objects.filter(
2021-03-08 16:49:10 +00:00
Q(
user_subject=self.user_subject,
user_object=self.user_object,
)
| Q(
user_subject=self.user_object,
user_object=self.user_subject,
)
).exists():
2021-02-15 20:51:34 +00:00
raise IntegrityError()
super().save(*args, **kwargs)
2021-02-11 00:06:50 +00:00
if broadcast and self.user_subject.local and not self.user_object.local:
self.broadcast(self.to_activity(), self.user_subject)
2021-02-11 00:06:50 +00:00
if self.user_object.local:
2021-02-16 04:49:23 +00:00
manually_approves = self.user_object.manually_approves_followers
if not manually_approves:
2021-02-16 04:49:23 +00:00
self.accept()
2021-03-08 16:49:10 +00:00
model = apps.get_model("bookwyrm.Notification", require_ready=True)
notification_type = "FOLLOW_REQUEST" if manually_approves else "FOLLOW"
2021-02-11 00:06:50 +00:00
model.objects.create(
user=self.user_object,
related_user=self.user_subject,
notification_type=notification_type,
)
def get_accept_reject_id(self, status):
2021-04-26 16:15:42 +00:00
"""get id for sending an accept or reject of a local user"""
base_path = self.user_object.remote_id
2021-09-18 18:32:00 +00:00
status_id = self.id or 0
return f"{base_path}#{status}/{status_id}"
def accept(self, broadcast_only=False):
2021-04-26 16:15:42 +00:00
"""turn this request into the real deal"""
user = self.user_object
2021-02-17 19:28:54 +00:00
if not self.user_subject.local:
activity = activitypub.Accept(
id=self.get_accept_reject_id(status="accepts"),
2021-02-17 19:28:54 +00:00
actor=self.user_object.remote_id,
2021-03-08 16:49:10 +00:00
object=self.to_activity(),
2021-02-17 19:28:54 +00:00
).serialize()
self.broadcast(activity, user)
if broadcast_only:
return
with transaction.atomic():
UserFollows.from_request(self)
self.delete()
def reject(self):
2021-04-26 16:15:42 +00:00
"""generate a Reject for this follow request"""
2021-02-17 22:37:20 +00:00
if self.user_object.local:
activity = activitypub.Reject(
id=self.get_accept_reject_id(status="rejects"),
2021-02-17 22:37:20 +00:00
actor=self.user_object.remote_id,
2021-03-08 16:49:10 +00:00
object=self.to_activity(),
2021-02-17 22:37:20 +00:00
).serialize()
self.broadcast(activity, self.user_object)
self.delete()
2020-09-17 20:09:11 +00:00
class UserBlocks(ActivityMixin, UserRelationship):
2021-04-26 16:15:42 +00:00
"""prevent another user from following you and seeing your posts"""
2021-03-08 16:49:10 +00:00
status = "blocks"
2021-01-23 19:03:10 +00:00
activity_serializer = activitypub.Block
2021-01-25 01:07:19 +00:00
def save(self, *args, **kwargs):
2021-04-26 16:15:42 +00:00
"""remove follow or follow request rels after a block is created"""
super().save(*args, **kwargs)
2021-01-25 01:07:19 +00:00
UserFollows.objects.filter(
2021-03-08 16:49:10 +00:00
Q(user_subject=self.user_subject, user_object=self.user_object)
| Q(user_subject=self.user_object, user_object=self.user_subject)
).delete()
UserFollowRequest.objects.filter(
2021-03-08 16:49:10 +00:00
Q(user_subject=self.user_subject, user_object=self.user_object)
| Q(user_subject=self.user_object, user_object=self.user_subject)
).delete()