We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Follow Request bug: looks as if you already follow a person if you go to profile of requester
When an account with moderated follows has a follow request incoming the following happens:

You see a notification about someone wanting to follow you. You go to their profile to see what they are like and... it looks like you already follow this person? Because the "Follow" button erroneously reads "Unfollow". 

Additionally, there is no other indication of a pending follow request while on their profile.

**To Reproduce**

1. Set your account to prompt follow requests
2. Get a follow request
3. Go to the profile of the requester
4. The follow button now reads "Unfollow", implying you follow them already (you don't)

As seen on bookwyrm.social

(anyway thanks for this excellent network of book nerds mouse!!)


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bookwyrm/models/relationship.py`
Content:
```
1 """ defines relationships between users """
2 from django.apps import apps
3 from django.core.cache import cache
4 from django.db import models, transaction, IntegrityError
5 from django.db.models import Q
6 
7 from bookwyrm import activitypub
8 from .activitypub_mixin import ActivitypubMixin, ActivityMixin
9 from .activitypub_mixin import generate_activity
10 from .base_model import BookWyrmModel
11 from . import fields
12 
13 
14 class UserRelationship(BookWyrmModel):
15     """many-to-many through table for followers"""
16 
17     user_subject = fields.ForeignKey(
18         "User",
19         on_delete=models.PROTECT,
20         related_name="%(class)s_user_subject",
21         activitypub_field="actor",
22     )
23     user_object = fields.ForeignKey(
24         "User",
25         on_delete=models.PROTECT,
26         related_name="%(class)s_user_object",
27         activitypub_field="object",
28     )
29 
30     @property
31     def privacy(self):
32         """all relationships are handled directly with the participants"""
33         return "direct"
34 
35     @property
36     def recipients(self):
37         """the remote user needs to recieve direct broadcasts"""
38         return [u for u in [self.user_subject, self.user_object] if not u.local]
39 
40     def save(self, *args, **kwargs):
41         """clear the template cache"""
42         clear_cache(self.user_subject, self.user_object)
43         super().save(*args, **kwargs)
44 
45     def delete(self, *args, **kwargs):
46         """clear the template cache"""
47         clear_cache(self.user_subject, self.user_object)
48         super().delete(*args, **kwargs)
49 
50     class Meta:
51         """relationships should be unique"""
52 
53         abstract = True
54         constraints = [
55             models.UniqueConstraint(
56                 fields=["user_subject", "user_object"], name="%(class)s_unique"
57             ),
58             models.CheckConstraint(
59                 check=~models.Q(user_subject=models.F("user_object")),
60                 name="%(class)s_no_self",
61             ),
62         ]
63 
64     def get_remote_id(self):
65         """use shelf identifier in remote_id"""
66         base_path = self.user_subject.remote_id
67         return f"{base_path}#follows/{self.id}"
68 
69 
70 class UserFollows(ActivityMixin, UserRelationship):
71     """Following a user"""
72 
73     status = "follows"
74 
75     def to_activity(self):  # pylint: disable=arguments-differ
76         """overrides default to manually set serializer"""
77         return activitypub.Follow(**generate_activity(self))
78 
79     def save(self, *args, **kwargs):
80         """really really don't let a user follow someone who blocked them"""
81         # blocking in either direction is a no-go
82         if UserBlocks.objects.filter(
83             Q(
84                 user_subject=self.user_subject,
85                 user_object=self.user_object,
86             )
87             | Q(
88                 user_subject=self.user_object,
89                 user_object=self.user_subject,
90             )
91         ).exists():
92             raise IntegrityError(
93                 "Attempting to follow blocked user", self.user_subject, self.user_object
94             )
95         # don't broadcast this type of relationship -- accepts and requests
96         # are handled by the UserFollowRequest model
97         super().save(*args, broadcast=False, **kwargs)
98 
99     @classmethod
100     def from_request(cls, follow_request):
101         """converts a follow request into a follow relationship"""
102         obj, _ = cls.objects.get_or_create(
103             user_subject=follow_request.user_subject,
104             user_object=follow_request.user_object,
105             remote_id=follow_request.remote_id,
106         )
107         return obj
108 
109 
110 class UserFollowRequest(ActivitypubMixin, UserRelationship):
111     """following a user requires manual or automatic confirmation"""
112 
113     status = "follow_request"
114     activity_serializer = activitypub.Follow
115 
116     def save(self, *args, broadcast=True, **kwargs):  # pylint: disable=arguments-differ
117         """make sure the follow or block relationship doesn't already exist"""
118         # if there's a request for a follow that already exists, accept it
119         # without changing the local database state
120         if UserFollows.objects.filter(
121             user_subject=self.user_subject,
122             user_object=self.user_object,
123         ).exists():
124             self.accept(broadcast_only=True)
125             return
126 
127         # blocking in either direction is a no-go
128         if UserBlocks.objects.filter(
129             Q(
130                 user_subject=self.user_subject,
131                 user_object=self.user_object,
132             )
133             | Q(
134                 user_subject=self.user_object,
135                 user_object=self.user_subject,
136             )
137         ).exists():
138             raise IntegrityError(
139                 "Attempting to follow blocked user", self.user_subject, self.user_object
140             )
141         super().save(*args, **kwargs)
142 
143         if broadcast and self.user_subject.local and not self.user_object.local:
144             self.broadcast(self.to_activity(), self.user_subject)
145 
146         if self.user_object.local:
147             manually_approves = self.user_object.manually_approves_followers
148             if not manually_approves:
149                 self.accept()
150 
151             model = apps.get_model("bookwyrm.Notification", require_ready=True)
152             notification_type = "FOLLOW_REQUEST" if manually_approves else "FOLLOW"
153             model.objects.create(
154                 user=self.user_object,
155                 related_user=self.user_subject,
156                 notification_type=notification_type,
157             )
158 
159     def get_accept_reject_id(self, status):
160         """get id for sending an accept or reject of a local user"""
161 
162         base_path = self.user_object.remote_id
163         status_id = self.id or 0
164         return f"{base_path}#{status}/{status_id}"
165 
166     def accept(self, broadcast_only=False):
167         """turn this request into the real deal"""
168         user = self.user_object
169         if not self.user_subject.local:
170             activity = activitypub.Accept(
171                 id=self.get_accept_reject_id(status="accepts"),
172                 actor=self.user_object.remote_id,
173                 object=self.to_activity(),
174             ).serialize()
175             self.broadcast(activity, user)
176         if broadcast_only:
177             return
178 
179         with transaction.atomic():
180             UserFollows.from_request(self)
181             if self.id:
182                 self.delete()
183 
184     def reject(self):
185         """generate a Reject for this follow request"""
186         if self.user_object.local:
187             activity = activitypub.Reject(
188                 id=self.get_accept_reject_id(status="rejects"),
189                 actor=self.user_object.remote_id,
190                 object=self.to_activity(),
191             ).serialize()
192             self.broadcast(activity, self.user_object)
193 
194         self.delete()
195 
196 
197 class UserBlocks(ActivityMixin, UserRelationship):
198     """prevent another user from following you and seeing your posts"""
199 
200     status = "blocks"
201     activity_serializer = activitypub.Block
202 
203     def save(self, *args, **kwargs):
204         """remove follow or follow request rels after a block is created"""
205         super().save(*args, **kwargs)
206 
207         UserFollows.objects.filter(
208             Q(user_subject=self.user_subject, user_object=self.user_object)
209             | Q(user_subject=self.user_object, user_object=self.user_subject)
210         ).delete()
211         UserFollowRequest.objects.filter(
212             Q(user_subject=self.user_subject, user_object=self.user_object)
213             | Q(user_subject=self.user_object, user_object=self.user_subject)
214         ).delete()
215 
216 
217 def clear_cache(user_subject, user_object):
218     """clear relationship cache"""
219     cache.delete_many(
220         [
221             f"relationship-{user_subject.id}-{user_object.id}",
222             f"relationship-{user_object.id}-{user_subject.id}",
223         ]
224     )
225
```

Path: `bookwyrm/templatetags/interaction.py`
Content:
```
1 """ template filters for status interaction buttons """
2 from django import template
3 
4 from bookwyrm import models
5 from bookwyrm.utils.cache import get_or_set
6 
7 
8 register = template.Library()
9 
10 
11 @register.filter(name="liked")
12 def get_user_liked(user, status):
13     """did the given user fav a status?"""
14     return get_or_set(
15         f"fav-{user.id}-{status.id}",
16         lambda u, s: models.Favorite.objects.filter(user=u, status=s).exists(),
17         user,
18         status,
19         timeout=259200,
20     )
21 
22 
23 @register.filter(name="boosted")
24 def get_user_boosted(user, status):
25     """did the given user fav a status?"""
26     return get_or_set(
27         f"boost-{user.id}-{status.id}",
28         lambda u: status.boosters.filter(user=u).exists(),
29         user,
30         timeout=259200,
31     )
32 
33 
34 @register.filter(name="saved")
35 def get_user_saved_lists(user, book_list):
36     """did the user save a list"""
37     return user.saved_lists.filter(id=book_list.id).exists()
38 
39 
40 @register.simple_tag(takes_context=True)
41 def get_relationship(context, user_object):
42     """caches the relationship between the logged in user and another user"""
43     user = context["request"].user
44     return get_or_set(
45         f"relationship-{user.id}-{user_object.id}",
46         get_relationship_name,
47         user,
48         user_object,
49         timeout=259200,
50     )
51 
52 
53 def get_relationship_name(user, user_object):
54     """returns the relationship type"""
55     types = {
56         "is_following": False,
57         "is_follow_pending": False,
58         "is_blocked": False,
59     }
60     if user_object in user.blocks.all():
61         types["is_blocked"] = True
62     elif user_object in user.following.all():
63         types["is_following"] = True
64     elif user_object in user.follower_requests.all():
65         types["is_follow_pending"] = True
66     return types
67
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py
--- a/bookwyrm/models/relationship.py
+++ b/bookwyrm/models/relationship.py
@@ -218,7 +218,7 @@
     """clear relationship cache"""
     cache.delete_many(
         [
-            f"relationship-{user_subject.id}-{user_object.id}",
-            f"relationship-{user_object.id}-{user_subject.id}",
+            f"cached-relationship-{user_subject.id}-{user_object.id}",
+            f"cached-relationship-{user_object.id}-{user_subject.id}",
         ]
     )
diff --git a/bookwyrm/templatetags/interaction.py b/bookwyrm/templatetags/interaction.py
--- a/bookwyrm/templatetags/interaction.py
+++ b/bookwyrm/templatetags/interaction.py
@@ -42,7 +42,7 @@
     """caches the relationship between the logged in user and another user"""
     user = context["request"].user
     return get_or_set(
-        f"relationship-{user.id}-{user_object.id}",
+        f"cached-relationship-{user.id}-{user_object.id}",
         get_relationship_name,
         user,
         user_object,
@@ -61,6 +61,6 @@
         types["is_blocked"] = True
     elif user_object in user.following.all():
         types["is_following"] = True
-    elif user_object in user.follower_requests.all():
+    elif user in user_object.follower_requests.all():
         types["is_follow_pending"] = True
     return types

{"golden_diff": "diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py\n--- a/bookwyrm/models/relationship.py\n+++ b/bookwyrm/models/relationship.py\n@@ -218,7 +218,7 @@\n     \"\"\"clear relationship cache\"\"\"\n     cache.delete_many(\n         [\n-            f\"relationship-{user_subject.id}-{user_object.id}\",\n-            f\"relationship-{user_object.id}-{user_subject.id}\",\n+            f\"cached-relationship-{user_subject.id}-{user_object.id}\",\n+            f\"cached-relationship-{user_object.id}-{user_subject.id}\",\n         ]\n     )\ndiff --git a/bookwyrm/templatetags/interaction.py b/bookwyrm/templatetags/interaction.py\n--- a/bookwyrm/templatetags/interaction.py\n+++ b/bookwyrm/templatetags/interaction.py\n@@ -42,7 +42,7 @@\n     \"\"\"caches the relationship between the logged in user and another user\"\"\"\n     user = context[\"request\"].user\n     return get_or_set(\n-        f\"relationship-{user.id}-{user_object.id}\",\n+        f\"cached-relationship-{user.id}-{user_object.id}\",\n         get_relationship_name,\n         user,\n         user_object,\n@@ -61,6 +61,6 @@\n         types[\"is_blocked\"] = True\n     elif user_object in user.following.all():\n         types[\"is_following\"] = True\n-    elif user_object in user.follower_requests.all():\n+    elif user in user_object.follower_requests.all():\n         types[\"is_follow_pending\"] = True\n     return types\n", "issue": "Follow Request bug: looks as if you already follow a person if you go to profile of requester\nWhen an account with moderated follows has a follow request incoming the following happens:\r\n\r\nYou see a notification about someone wanting to follow you. You go to their profile to see what they are like and... it looks like you already follow this person? Because the \"Follow\" button erroneously reads \"Unfollow\". \r\n\r\nAdditionally, there is no other indication of a pending follow request while on their profile.\r\n\r\n**To Reproduce**\r\n\r\n1. Set your account to prompt follow requests\r\n2. Get a follow request\r\n3. Go to the profile of the requester\r\n4. The follow button now reads \"Unfollow\", implying you follow them already (you don't)\r\n\r\nAs seen on bookwyrm.social\r\n\r\n(anyway thanks for this excellent network of book nerds mouse!!)\r\n\n", "before_files": [{"content": "\"\"\" defines relationships between users \"\"\"\nfrom django.apps import apps\nfrom django.core.cache import cache\nfrom django.db import models, transaction, IntegrityError\nfrom django.db.models import Q\n\nfrom bookwyrm import activitypub\nfrom .activitypub_mixin import ActivitypubMixin, ActivityMixin\nfrom .activitypub_mixin import generate_activity\nfrom .base_model import BookWyrmModel\nfrom . import fields\n\n\nclass UserRelationship(BookWyrmModel):\n    \"\"\"many-to-many through table for followers\"\"\"\n\n    user_subject = fields.ForeignKey(\n        \"User\",\n        on_delete=models.PROTECT,\n        related_name=\"%(class)s_user_subject\",\n        activitypub_field=\"actor\",\n    )\n    user_object = fields.ForeignKey(\n        \"User\",\n        on_delete=models.PROTECT,\n        related_name=\"%(class)s_user_object\",\n        activitypub_field=\"object\",\n    )\n\n    @property\n    def privacy(self):\n        \"\"\"all relationships are handled directly with the participants\"\"\"\n        return \"direct\"\n\n    @property\n    def recipients(self):\n        \"\"\"the remote user needs to recieve direct broadcasts\"\"\"\n        return [u for u in [self.user_subject, self.user_object] if not u.local]\n\n    def save(self, *args, **kwargs):\n        \"\"\"clear the template cache\"\"\"\n        clear_cache(self.user_subject, self.user_object)\n        super().save(*args, **kwargs)\n\n    def delete(self, *args, **kwargs):\n        \"\"\"clear the template cache\"\"\"\n        clear_cache(self.user_subject, self.user_object)\n        super().delete(*args, **kwargs)\n\n    class Meta:\n        \"\"\"relationships should be unique\"\"\"\n\n        abstract = True\n        constraints = [\n            models.UniqueConstraint(\n                fields=[\"user_subject\", \"user_object\"], name=\"%(class)s_unique\"\n            ),\n            models.CheckConstraint(\n                check=~models.Q(user_subject=models.F(\"user_object\")),\n                name=\"%(class)s_no_self\",\n            ),\n        ]\n\n    def get_remote_id(self):\n        \"\"\"use shelf identifier in remote_id\"\"\"\n        base_path = self.user_subject.remote_id\n        return f\"{base_path}#follows/{self.id}\"\n\n\nclass UserFollows(ActivityMixin, UserRelationship):\n    \"\"\"Following a user\"\"\"\n\n    status = \"follows\"\n\n    def to_activity(self):  # pylint: disable=arguments-differ\n        \"\"\"overrides default to manually set serializer\"\"\"\n        return activitypub.Follow(**generate_activity(self))\n\n    def save(self, *args, **kwargs):\n        \"\"\"really really don't let a user follow someone who blocked them\"\"\"\n        # blocking in either direction is a no-go\n        if UserBlocks.objects.filter(\n            Q(\n                user_subject=self.user_subject,\n                user_object=self.user_object,\n            )\n            | Q(\n                user_subject=self.user_object,\n                user_object=self.user_subject,\n            )\n        ).exists():\n            raise IntegrityError(\n                \"Attempting to follow blocked user\", self.user_subject, self.user_object\n            )\n        # don't broadcast this type of relationship -- accepts and requests\n        # are handled by the UserFollowRequest model\n        super().save(*args, broadcast=False, **kwargs)\n\n    @classmethod\n    def from_request(cls, follow_request):\n        \"\"\"converts a follow request into a follow relationship\"\"\"\n        obj, _ = cls.objects.get_or_create(\n            user_subject=follow_request.user_subject,\n            user_object=follow_request.user_object,\n            remote_id=follow_request.remote_id,\n        )\n        return obj\n\n\nclass UserFollowRequest(ActivitypubMixin, UserRelationship):\n    \"\"\"following a user requires manual or automatic confirmation\"\"\"\n\n    status = \"follow_request\"\n    activity_serializer = activitypub.Follow\n\n    def save(self, *args, broadcast=True, **kwargs):  # pylint: disable=arguments-differ\n        \"\"\"make sure the follow or block relationship doesn't already exist\"\"\"\n        # if there's a request for a follow that already exists, accept it\n        # without changing the local database state\n        if UserFollows.objects.filter(\n            user_subject=self.user_subject,\n            user_object=self.user_object,\n        ).exists():\n            self.accept(broadcast_only=True)\n            return\n\n        # blocking in either direction is a no-go\n        if UserBlocks.objects.filter(\n            Q(\n                user_subject=self.user_subject,\n                user_object=self.user_object,\n            )\n            | Q(\n                user_subject=self.user_object,\n                user_object=self.user_subject,\n            )\n        ).exists():\n            raise IntegrityError(\n                \"Attempting to follow blocked user\", self.user_subject, self.user_object\n            )\n        super().save(*args, **kwargs)\n\n        if broadcast and self.user_subject.local and not self.user_object.local:\n            self.broadcast(self.to_activity(), self.user_subject)\n\n        if self.user_object.local:\n            manually_approves = self.user_object.manually_approves_followers\n            if not manually_approves:\n                self.accept()\n\n            model = apps.get_model(\"bookwyrm.Notification\", require_ready=True)\n            notification_type = \"FOLLOW_REQUEST\" if manually_approves else \"FOLLOW\"\n            model.objects.create(\n                user=self.user_object,\n                related_user=self.user_subject,\n                notification_type=notification_type,\n            )\n\n    def get_accept_reject_id(self, status):\n        \"\"\"get id for sending an accept or reject of a local user\"\"\"\n\n        base_path = self.user_object.remote_id\n        status_id = self.id or 0\n        return f\"{base_path}#{status}/{status_id}\"\n\n    def accept(self, broadcast_only=False):\n        \"\"\"turn this request into the real deal\"\"\"\n        user = self.user_object\n        if not self.user_subject.local:\n            activity = activitypub.Accept(\n                id=self.get_accept_reject_id(status=\"accepts\"),\n                actor=self.user_object.remote_id,\n                object=self.to_activity(),\n            ).serialize()\n            self.broadcast(activity, user)\n        if broadcast_only:\n            return\n\n        with transaction.atomic():\n            UserFollows.from_request(self)\n            if self.id:\n                self.delete()\n\n    def reject(self):\n        \"\"\"generate a Reject for this follow request\"\"\"\n        if self.user_object.local:\n            activity = activitypub.Reject(\n                id=self.get_accept_reject_id(status=\"rejects\"),\n                actor=self.user_object.remote_id,\n                object=self.to_activity(),\n            ).serialize()\n            self.broadcast(activity, self.user_object)\n\n        self.delete()\n\n\nclass UserBlocks(ActivityMixin, UserRelationship):\n    \"\"\"prevent another user from following you and seeing your posts\"\"\"\n\n    status = \"blocks\"\n    activity_serializer = activitypub.Block\n\n    def save(self, *args, **kwargs):\n        \"\"\"remove follow or follow request rels after a block is created\"\"\"\n        super().save(*args, **kwargs)\n\n        UserFollows.objects.filter(\n            Q(user_subject=self.user_subject, user_object=self.user_object)\n            | Q(user_subject=self.user_object, user_object=self.user_subject)\n        ).delete()\n        UserFollowRequest.objects.filter(\n            Q(user_subject=self.user_subject, user_object=self.user_object)\n            | Q(user_subject=self.user_object, user_object=self.user_subject)\n        ).delete()\n\n\ndef clear_cache(user_subject, user_object):\n    \"\"\"clear relationship cache\"\"\"\n    cache.delete_many(\n        [\n            f\"relationship-{user_subject.id}-{user_object.id}\",\n            f\"relationship-{user_object.id}-{user_subject.id}\",\n        ]\n    )\n", "path": "bookwyrm/models/relationship.py"}, {"content": "\"\"\" template filters for status interaction buttons \"\"\"\nfrom django import template\n\nfrom bookwyrm import models\nfrom bookwyrm.utils.cache import get_or_set\n\n\nregister = template.Library()\n\n\n@register.filter(name=\"liked\")\ndef get_user_liked(user, status):\n    \"\"\"did the given user fav a status?\"\"\"\n    return get_or_set(\n        f\"fav-{user.id}-{status.id}\",\n        lambda u, s: models.Favorite.objects.filter(user=u, status=s).exists(),\n        user,\n        status,\n        timeout=259200,\n    )\n\n\n@register.filter(name=\"boosted\")\ndef get_user_boosted(user, status):\n    \"\"\"did the given user fav a status?\"\"\"\n    return get_or_set(\n        f\"boost-{user.id}-{status.id}\",\n        lambda u: status.boosters.filter(user=u).exists(),\n        user,\n        timeout=259200,\n    )\n\n\n@register.filter(name=\"saved\")\ndef get_user_saved_lists(user, book_list):\n    \"\"\"did the user save a list\"\"\"\n    return user.saved_lists.filter(id=book_list.id).exists()\n\n\n@register.simple_tag(takes_context=True)\ndef get_relationship(context, user_object):\n    \"\"\"caches the relationship between the logged in user and another user\"\"\"\n    user = context[\"request\"].user\n    return get_or_set(\n        f\"relationship-{user.id}-{user_object.id}\",\n        get_relationship_name,\n        user,\n        user_object,\n        timeout=259200,\n    )\n\n\ndef get_relationship_name(user, user_object):\n    \"\"\"returns the relationship type\"\"\"\n    types = {\n        \"is_following\": False,\n        \"is_follow_pending\": False,\n        \"is_blocked\": False,\n    }\n    if user_object in user.blocks.all():\n        types[\"is_blocked\"] = True\n    elif user_object in user.following.all():\n        types[\"is_following\"] = True\n    elif user_object in user.follower_requests.all():\n        types[\"is_follow_pending\"] = True\n    return types\n", "path": "bookwyrm/templatetags/interaction.py"}], "after_files": [{"content": "\"\"\" defines relationships between users \"\"\"\nfrom django.apps import apps\nfrom django.core.cache import cache\nfrom django.db import models, transaction, IntegrityError\nfrom django.db.models import Q\n\nfrom bookwyrm import activitypub\nfrom .activitypub_mixin import ActivitypubMixin, ActivityMixin\nfrom .activitypub_mixin import generate_activity\nfrom .base_model import BookWyrmModel\nfrom . import fields\n\n\nclass UserRelationship(BookWyrmModel):\n    \"\"\"many-to-many through table for followers\"\"\"\n\n    user_subject = fields.ForeignKey(\n        \"User\",\n        on_delete=models.PROTECT,\n        related_name=\"%(class)s_user_subject\",\n        activitypub_field=\"actor\",\n    )\n    user_object = fields.ForeignKey(\n        \"User\",\n        on_delete=models.PROTECT,\n        related_name=\"%(class)s_user_object\",\n        activitypub_field=\"object\",\n    )\n\n    @property\n    def privacy(self):\n        \"\"\"all relationships are handled directly with the participants\"\"\"\n        return \"direct\"\n\n    @property\n    def recipients(self):\n        \"\"\"the remote user needs to recieve direct broadcasts\"\"\"\n        return [u for u in [self.user_subject, self.user_object] if not u.local]\n\n    def save(self, *args, **kwargs):\n        \"\"\"clear the template cache\"\"\"\n        clear_cache(self.user_subject, self.user_object)\n        super().save(*args, **kwargs)\n\n    def delete(self, *args, **kwargs):\n        \"\"\"clear the template cache\"\"\"\n        clear_cache(self.user_subject, self.user_object)\n        super().delete(*args, **kwargs)\n\n    class Meta:\n        \"\"\"relationships should be unique\"\"\"\n\n        abstract = True\n        constraints = [\n            models.UniqueConstraint(\n                fields=[\"user_subject\", \"user_object\"], name=\"%(class)s_unique\"\n            ),\n            models.CheckConstraint(\n                check=~models.Q(user_subject=models.F(\"user_object\")),\n                name=\"%(class)s_no_self\",\n            ),\n        ]\n\n    def get_remote_id(self):\n        \"\"\"use shelf identifier in remote_id\"\"\"\n        base_path = self.user_subject.remote_id\n        return f\"{base_path}#follows/{self.id}\"\n\n\nclass UserFollows(ActivityMixin, UserRelationship):\n    \"\"\"Following a user\"\"\"\n\n    status = \"follows\"\n\n    def to_activity(self):  # pylint: disable=arguments-differ\n        \"\"\"overrides default to manually set serializer\"\"\"\n        return activitypub.Follow(**generate_activity(self))\n\n    def save(self, *args, **kwargs):\n        \"\"\"really really don't let a user follow someone who blocked them\"\"\"\n        # blocking in either direction is a no-go\n        if UserBlocks.objects.filter(\n            Q(\n                user_subject=self.user_subject,\n                user_object=self.user_object,\n            )\n            | Q(\n                user_subject=self.user_object,\n                user_object=self.user_subject,\n            )\n        ).exists():\n            raise IntegrityError(\n                \"Attempting to follow blocked user\", self.user_subject, self.user_object\n            )\n        # don't broadcast this type of relationship -- accepts and requests\n        # are handled by the UserFollowRequest model\n        super().save(*args, broadcast=False, **kwargs)\n\n    @classmethod\n    def from_request(cls, follow_request):\n        \"\"\"converts a follow request into a follow relationship\"\"\"\n        obj, _ = cls.objects.get_or_create(\n            user_subject=follow_request.user_subject,\n            user_object=follow_request.user_object,\n            remote_id=follow_request.remote_id,\n        )\n        return obj\n\n\nclass UserFollowRequest(ActivitypubMixin, UserRelationship):\n    \"\"\"following a user requires manual or automatic confirmation\"\"\"\n\n    status = \"follow_request\"\n    activity_serializer = activitypub.Follow\n\n    def save(self, *args, broadcast=True, **kwargs):  # pylint: disable=arguments-differ\n        \"\"\"make sure the follow or block relationship doesn't already exist\"\"\"\n        # if there's a request for a follow that already exists, accept it\n        # without changing the local database state\n        if UserFollows.objects.filter(\n            user_subject=self.user_subject,\n            user_object=self.user_object,\n        ).exists():\n            self.accept(broadcast_only=True)\n            return\n\n        # blocking in either direction is a no-go\n        if UserBlocks.objects.filter(\n            Q(\n                user_subject=self.user_subject,\n                user_object=self.user_object,\n            )\n            | Q(\n                user_subject=self.user_object,\n                user_object=self.user_subject,\n            )\n        ).exists():\n            raise IntegrityError(\n                \"Attempting to follow blocked user\", self.user_subject, self.user_object\n            )\n        super().save(*args, **kwargs)\n\n        if broadcast and self.user_subject.local and not self.user_object.local:\n            self.broadcast(self.to_activity(), self.user_subject)\n\n        if self.user_object.local:\n            manually_approves = self.user_object.manually_approves_followers\n            if not manually_approves:\n                self.accept()\n\n            model = apps.get_model(\"bookwyrm.Notification\", require_ready=True)\n            notification_type = \"FOLLOW_REQUEST\" if manually_approves else \"FOLLOW\"\n            model.objects.create(\n                user=self.user_object,\n                related_user=self.user_subject,\n                notification_type=notification_type,\n            )\n\n    def get_accept_reject_id(self, status):\n        \"\"\"get id for sending an accept or reject of a local user\"\"\"\n\n        base_path = self.user_object.remote_id\n        status_id = self.id or 0\n        return f\"{base_path}#{status}/{status_id}\"\n\n    def accept(self, broadcast_only=False):\n        \"\"\"turn this request into the real deal\"\"\"\n        user = self.user_object\n        if not self.user_subject.local:\n            activity = activitypub.Accept(\n                id=self.get_accept_reject_id(status=\"accepts\"),\n                actor=self.user_object.remote_id,\n                object=self.to_activity(),\n            ).serialize()\n            self.broadcast(activity, user)\n        if broadcast_only:\n            return\n\n        with transaction.atomic():\n            UserFollows.from_request(self)\n            if self.id:\n                self.delete()\n\n    def reject(self):\n        \"\"\"generate a Reject for this follow request\"\"\"\n        if self.user_object.local:\n            activity = activitypub.Reject(\n                id=self.get_accept_reject_id(status=\"rejects\"),\n                actor=self.user_object.remote_id,\n                object=self.to_activity(),\n            ).serialize()\n            self.broadcast(activity, self.user_object)\n\n        self.delete()\n\n\nclass UserBlocks(ActivityMixin, UserRelationship):\n    \"\"\"prevent another user from following you and seeing your posts\"\"\"\n\n    status = \"blocks\"\n    activity_serializer = activitypub.Block\n\n    def save(self, *args, **kwargs):\n        \"\"\"remove follow or follow request rels after a block is created\"\"\"\n        super().save(*args, **kwargs)\n\n        UserFollows.objects.filter(\n            Q(user_subject=self.user_subject, user_object=self.user_object)\n            | Q(user_subject=self.user_object, user_object=self.user_subject)\n        ).delete()\n        UserFollowRequest.objects.filter(\n            Q(user_subject=self.user_subject, user_object=self.user_object)\n            | Q(user_subject=self.user_object, user_object=self.user_subject)\n        ).delete()\n\n\ndef clear_cache(user_subject, user_object):\n    \"\"\"clear relationship cache\"\"\"\n    cache.delete_many(\n        [\n            f\"cached-relationship-{user_subject.id}-{user_object.id}\",\n            f\"cached-relationship-{user_object.id}-{user_subject.id}\",\n        ]\n    )\n", "path": "bookwyrm/models/relationship.py"}, {"content": "\"\"\" template filters for status interaction buttons \"\"\"\nfrom django import template\n\nfrom bookwyrm import models\nfrom bookwyrm.utils.cache import get_or_set\n\n\nregister = template.Library()\n\n\n@register.filter(name=\"liked\")\ndef get_user_liked(user, status):\n    \"\"\"did the given user fav a status?\"\"\"\n    return get_or_set(\n        f\"fav-{user.id}-{status.id}\",\n        lambda u, s: models.Favorite.objects.filter(user=u, status=s).exists(),\n        user,\n        status,\n        timeout=259200,\n    )\n\n\n@register.filter(name=\"boosted\")\ndef get_user_boosted(user, status):\n    \"\"\"did the given user fav a status?\"\"\"\n    return get_or_set(\n        f\"boost-{user.id}-{status.id}\",\n        lambda u: status.boosters.filter(user=u).exists(),\n        user,\n        timeout=259200,\n    )\n\n\n@register.filter(name=\"saved\")\ndef get_user_saved_lists(user, book_list):\n    \"\"\"did the user save a list\"\"\"\n    return user.saved_lists.filter(id=book_list.id).exists()\n\n\n@register.simple_tag(takes_context=True)\ndef get_relationship(context, user_object):\n    \"\"\"caches the relationship between the logged in user and another user\"\"\"\n    user = context[\"request\"].user\n    return get_or_set(\n        f\"cached-relationship-{user.id}-{user_object.id}\",\n        get_relationship_name,\n        user,\n        user_object,\n        timeout=259200,\n    )\n\n\ndef get_relationship_name(user, user_object):\n    \"\"\"returns the relationship type\"\"\"\n    types = {\n        \"is_following\": False,\n        \"is_follow_pending\": False,\n        \"is_blocked\": False,\n    }\n    if user_object in user.blocks.all():\n        types[\"is_blocked\"] = True\n    elif user_object in user.following.all():\n        types[\"is_following\"] = True\n    elif user in user_object.follower_requests.all():\n        types[\"is_follow_pending\"] = True\n    return types\n", "path": "bookwyrm/templatetags/interaction.py"}]}