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
|
|
|
|
2020-09-21 15:10:37 +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
|
2021-02-04 18:47:03 +00:00
|
|
|
from .base_model import BookWyrmModel
|
2020-11-30 22:54:45 +00:00
|
|
|
from . import fields
|
2020-09-17 20:09:11 +00:00
|
|
|
|
|
|
|
|
2021-02-07 03:12:49 +00:00
|
|
|
class UserRelationship(BookWyrmModel):
|
2021-03-08 16:49:10 +00:00
|
|
|
""" many-to-many through table for followers """
|
|
|
|
|
2020-11-30 22:54:45 +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
|
|
|
)
|
2020-11-30 22:54:45 +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
|
|
|
)
|
|
|
|
|
2021-02-07 03:12:49 +00:00
|
|
|
@property
|
|
|
|
def privacy(self):
|
2021-03-08 16:49:10 +00:00
|
|
|
""" all relationships are handled directly with the participants """
|
|
|
|
return "direct"
|
2021-02-07 03:12:49 +00:00
|
|
|
|
2021-02-09 18:26:04 +00:00
|
|
|
@property
|
|
|
|
def recipients(self):
|
2021-03-08 16:49:10 +00:00
|
|
|
""" the remote user needs to recieve direct broadcasts """
|
2021-02-09 18:26:04 +00:00
|
|
|
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-03-08 16:49:10 +00:00
|
|
|
""" relationships should be unique """
|
|
|
|
|
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
|
|
|
]
|
|
|
|
|
2021-04-18 14:30:01 +00:00
|
|
|
def get_remote_id(self):
|
2021-03-08 16:49:10 +00:00
|
|
|
""" use shelf identifier in remote_id """
|
2020-09-17 20:09:11 +00:00
|
|
|
base_path = self.user_subject.remote_id
|
2021-04-18 14:30:01 +00:00
|
|
|
return "%s#follows/%d" % (base_path, self.id)
|
2020-10-16 17:37:33 +00:00
|
|
|
|
2020-09-17 20:09:11 +00:00
|
|
|
|
2021-02-17 19:45:21 +00:00
|
|
|
class UserFollows(ActivityMixin, UserRelationship):
|
2021-03-08 16:49:10 +00:00
|
|
|
""" Following a user """
|
|
|
|
|
|
|
|
status = "follows"
|
2020-09-17 20:09:11 +00:00
|
|
|
|
2021-03-23 03:32:59 +00:00
|
|
|
def to_activity(self): # pylint: disable=arguments-differ
|
2021-03-08 16:49:10 +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-03-08 16:49:10 +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()
|
2021-02-24 17:51:34 +00:00
|
|
|
# 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-03-08 16:49:10 +00:00
|
|
|
""" converts a follow request into a follow relationship """
|
2021-02-07 03:12:49 +00:00
|
|
|
return cls.objects.create(
|
2020-09-17 20:09:11 +00:00
|
|
|
user_subject=follow_request.user_subject,
|
|
|
|
user_object=follow_request.user_object,
|
2020-10-16 17:37:33 +00:00
|
|
|
remote_id=follow_request.remote_id,
|
2020-09-17 20:09:11 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-02-07 03:12:49 +00:00
|
|
|
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
2021-03-08 16:49:10 +00:00
|
|
|
""" following a user requires manual or automatic confirmation """
|
|
|
|
|
|
|
|
status = "follow_request"
|
2021-02-07 03:12:49 +00:00
|
|
|
activity_serializer = activitypub.Follow
|
2020-09-17 20:09:11 +00:00
|
|
|
|
2021-02-09 18:26:04 +00:00
|
|
|
def save(self, *args, broadcast=True, **kwargs):
|
2021-03-08 16:49:10 +00:00
|
|
|
""" make sure the follow or block relationship doesn't already exist """
|
2021-04-22 15:40:32 +00:00
|
|
|
# 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():
|
2021-04-22 15:40:32 +00:00
|
|
|
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()
|
2021-02-17 20:23:55 +00:00
|
|
|
super().save(*args, **kwargs)
|
2021-02-11 00:06:50 +00:00
|
|
|
|
2021-02-08 00:23:20 +00:00
|
|
|
if broadcast and self.user_subject.local and not self.user_object.local:
|
2021-02-07 03:12:49 +00:00
|
|
|
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
|
2021-02-16 20:58:29 +00:00
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
2021-04-18 14:30:01 +00:00
|
|
|
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
|
2021-04-22 15:40:32 +00:00
|
|
|
return "%s#%s/%d" % (base_path, status, self.id or 0)
|
2021-04-18 14:30:01 +00:00
|
|
|
|
2021-04-22 15:40:32 +00:00
|
|
|
def accept(self, broadcast_only=False):
|
2021-03-08 16:49:10 +00:00
|
|
|
""" turn this request into the real deal"""
|
2021-02-07 03:12:49 +00:00
|
|
|
user = self.user_object
|
2021-02-17 19:28:54 +00:00
|
|
|
if not self.user_subject.local:
|
|
|
|
activity = activitypub.Accept(
|
2021-04-18 14:30:01 +00:00
|
|
|
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)
|
2021-04-22 15:40:32 +00:00
|
|
|
if broadcast_only:
|
|
|
|
return
|
|
|
|
|
2021-02-07 03:12:49 +00:00
|
|
|
with transaction.atomic():
|
|
|
|
UserFollows.from_request(self)
|
|
|
|
self.delete()
|
|
|
|
|
|
|
|
def reject(self):
|
2021-03-08 16:49:10 +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(
|
2021-04-18 14:30:01 +00:00
|
|
|
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)
|
|
|
|
|
2021-02-07 03:12:49 +00:00
|
|
|
self.delete()
|
2020-11-01 20:49:07 +00:00
|
|
|
|
2020-09-17 20:09:11 +00:00
|
|
|
|
2021-02-07 03:12:49 +00:00
|
|
|
class UserBlocks(ActivityMixin, UserRelationship):
|
2021-03-08 16:49:10 +00:00
|
|
|
""" prevent another user from following you and seeing your posts """
|
|
|
|
|
|
|
|
status = "blocks"
|
2021-01-23 19:03:10 +00:00
|
|
|
activity_serializer = activitypub.Block
|
2021-01-25 01:07:19 +00:00
|
|
|
|
2021-02-15 20:21:48 +00:00
|
|
|
def save(self, *args, **kwargs):
|
2021-03-08 16:49:10 +00:00
|
|
|
""" remove follow or follow request rels after a block is created """
|
2021-02-15 20:21:48 +00:00
|
|
|
super().save(*args, **kwargs)
|
2021-01-25 01:07:19 +00:00
|
|
|
|
2021-02-15 20:21:48 +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)
|
2021-02-15 20:21:48 +00:00
|
|
|
).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)
|
2021-02-15 20:21:48 +00:00
|
|
|
).delete()
|