""" defines relationships between users """ from django.core.cache import cache from django.db import models, transaction, IntegrityError from django.db.models import Q from bookwyrm import activitypub from .activitypub_mixin import ActivitypubMixin, ActivityMixin from .activitypub_mixin import generate_activity from .base_model import BookWyrmModel from . import fields class UserRelationship(BookWyrmModel): """many-to-many through table for followers""" user_subject = fields.ForeignKey( "User", on_delete=models.PROTECT, related_name="%(class)s_user_subject", activitypub_field="actor", ) user_object = fields.ForeignKey( "User", on_delete=models.PROTECT, related_name="%(class)s_user_object", activitypub_field="object", ) @property def privacy(self): """all relationships are handled directly with the participants""" return "direct" @property def recipients(self): """the remote user needs to receive direct broadcasts""" return [u for u in [self.user_subject, self.user_object] if not u.local] def save(self, *args, **kwargs): """clear the template cache""" clear_cache(self.user_subject, self.user_object) super().save(*args, **kwargs) def delete(self, *args, **kwargs): """clear the template cache""" clear_cache(self.user_subject, self.user_object) super().delete(*args, **kwargs) class Meta: """relationships should be unique""" abstract = True constraints = [ models.UniqueConstraint( fields=["user_subject", "user_object"], name="%(class)s_unique" ), models.CheckConstraint( check=~models.Q(user_subject=models.F("user_object")), name="%(class)s_no_self", ), ] def get_remote_id(self): """use shelf identifier in remote_id""" base_path = self.user_subject.remote_id return f"{base_path}#follows/{self.id}" def get_accept_reject_id(self, status): """get id for sending an accept or reject of a local user""" base_path = self.user_object.remote_id status_id = self.id or 0 return f"{base_path}#{status}/{status_id}" class UserFollows(ActivityMixin, UserRelationship): """Following a user""" status = "follows" def to_activity(self): # pylint: disable=arguments-differ """overrides default to manually set serializer""" return activitypub.Follow(**generate_activity(self)) def save(self, *args, **kwargs): """really really don't let a user follow someone who blocked them""" # blocking in either direction is a no-go if UserBlocks.objects.filter( Q( user_subject=self.user_subject, user_object=self.user_object, ) | Q( user_subject=self.user_object, user_object=self.user_subject, ) ).exists(): raise IntegrityError( "Attempting to follow blocked user", self.user_subject, self.user_object ) # don't broadcast this type of relationship -- accepts and requests # are handled by the UserFollowRequest model super().save(*args, broadcast=False, **kwargs) @classmethod def from_request(cls, follow_request): """converts a follow request into a follow relationship""" obj, _ = cls.objects.get_or_create( user_subject=follow_request.user_subject, user_object=follow_request.user_object, remote_id=follow_request.remote_id, ) return obj def reject(self): """generate a Reject for this follow. This would normally happen when a user deletes a follow they previously accepted""" if self.user_object.local: activity = activitypub.Reject( id=self.get_accept_reject_id(status="rejects"), actor=self.user_object.remote_id, object=self.to_activity(), ).serialize() self.broadcast(activity, self.user_object) self.delete() class UserFollowRequest(ActivitypubMixin, UserRelationship): """following a user requires manual or automatic confirmation""" status = "follow_request" activity_serializer = activitypub.Follow def save(self, *args, broadcast=True, **kwargs): # pylint: disable=arguments-differ """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 if UserFollows.objects.filter( user_subject=self.user_subject, user_object=self.user_object, ).exists(): self.accept(broadcast_only=True) return # blocking in either direction is a no-go if UserBlocks.objects.filter( Q( user_subject=self.user_subject, user_object=self.user_object, ) | Q( user_subject=self.user_object, user_object=self.user_subject, ) ).exists(): raise IntegrityError( "Attempting to follow blocked user", self.user_subject, self.user_object ) super().save(*args, **kwargs) # a local user is following a remote user if broadcast and self.user_subject.local and not self.user_object.local: self.broadcast(self.to_activity(), self.user_subject) if self.user_object.local: manually_approves = self.user_object.manually_approves_followers if not manually_approves: self.accept() def accept(self, broadcast_only=False): """turn this request into the real deal""" user = self.user_object # broadcast when accepting a remote request if not self.user_subject.local: activity = activitypub.Accept( id=self.get_accept_reject_id(status="accepts"), actor=self.user_object.remote_id, object=self.to_activity(), ).serialize() self.broadcast(activity, user) if broadcast_only: return with transaction.atomic(): try: UserFollows.from_request(self) except IntegrityError: # this just means we already saved this relationship pass if self.id: self.delete() def reject(self): """generate a Reject for this follow request""" if self.user_object.local: activity = activitypub.Reject( id=self.get_accept_reject_id(status="rejects"), actor=self.user_object.remote_id, object=self.to_activity(), ).serialize() self.broadcast(activity, self.user_object) self.delete() class UserBlocks(ActivityMixin, UserRelationship): """prevent another user from following you and seeing your posts""" status = "blocks" activity_serializer = activitypub.Block def save(self, *args, **kwargs): """remove follow or follow request rels after a block is created""" super().save(*args, **kwargs) UserFollows.objects.filter( 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( Q(user_subject=self.user_subject, user_object=self.user_object) | Q(user_subject=self.user_object, user_object=self.user_subject) ).delete() def clear_cache(user_subject, user_object): """clear relationship cache""" cache.delete_many( [ f"cached-relationship-{user_subject.id}-{user_object.id}", f"cached-relationship-{user_object.id}-{user_subject.id}", ] )