Repository: bookwyrm-social/bookwyrm

Issue:
Some follow requests fail 
**Describe the bug**

When trying to follow a user via suggestion box I get this:

```
db_1              | 2022-02-05 06:27:41.846 UTC [43264] ERROR:  duplicate key value violates unique constraint "userfollowrequest_unique"
db_1              | 2022-02-05 06:27:41.846 UTC [43264] DETAIL:  Key (user_subject_id, user_object_id)=(1, 3) already exists.
db_1              | 2022-02-05 06:27:41.846 UTC [43264] STATEMENT:  INSERT INTO "bookwyrm_userfollowrequest" ("created_date", "updated_date", "remote_id", "user_subject_id", "user_object_id") VALUES ('2022-02-05T06:27:41.843902+00:00'::timestamptz, '2022-02-05T06:27:41.843933+00:00'::timestamptz, NULL, 1, 3) RETURNING "bookwyrm_userfollowrequest"."id"
```

it does not happen for all the users, just for some of them. might be related that it is not the first time i am trying to follow the user and remote instance was not accessible or malfunctioned

**Expected behavior**
User gets a follow request



Discussion:

Resolving patch:
diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py
--- a/bookwyrm/models/relationship.py
+++ b/bookwyrm/models/relationship.py
@@ -39,15 +39,14 @@ def recipients(self):
 
     def save(self, *args, **kwargs):
         """clear the template cache"""
-        # invalidate the template cache
-        cache.delete_many(
-            [
-                f"relationship-{self.user_subject.id}-{self.user_object.id}",
-                f"relationship-{self.user_object.id}-{self.user_subject.id}",
-            ]
-        )
+        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"""
 
@@ -90,7 +89,9 @@ def save(self, *args, **kwargs):
                 user_object=self.user_subject,
             )
         ).exists():
-            raise IntegrityError()
+            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)
@@ -98,11 +99,12 @@ def save(self, *args, **kwargs):
     @classmethod
     def from_request(cls, follow_request):
         """converts a follow request into a follow relationship"""
-        return cls.objects.create(
+        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
 
 
 class UserFollowRequest(ActivitypubMixin, UserRelationship):
@@ -133,7 +135,9 @@ def save(self, *args, broadcast=True, **kwargs):  # pylint: disable=arguments-di
                 user_object=self.user_subject,
             )
         ).exists():
-            raise IntegrityError()
+            raise IntegrityError(
+                "Attempting to follow blocked user", self.user_subject, self.user_object
+            )
         super().save(*args, **kwargs)
 
         if broadcast and self.user_subject.local and not self.user_object.local:
@@ -174,7 +178,8 @@ def accept(self, broadcast_only=False):
 
         with transaction.atomic():
             UserFollows.from_request(self)
-            self.delete()
+            if self.id:
+                self.delete()
 
     def reject(self):
         """generate a Reject for this follow request"""
@@ -207,3 +212,13 @@ def save(self, *args, **kwargs):
             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"relationship-{user_subject.id}-{user_object.id}",
+            f"relationship-{user_object.id}-{user_subject.id}",
+        ]
+    )
diff --git a/bookwyrm/views/follow.py b/bookwyrm/views/follow.py
--- a/bookwyrm/views/follow.py
+++ b/bookwyrm/views/follow.py
@@ -2,12 +2,12 @@
 import urllib.parse
 import re
 from django.contrib.auth.decorators import login_required
-from django.db import IntegrityError
 from django.shortcuts import get_object_or_404, redirect
 from django.template.response import TemplateResponse
 from django.views.decorators.http import require_POST
 
 from bookwyrm import models
+from bookwyrm.models.relationship import clear_cache
 from .helpers import (
     get_user_from_username,
     handle_remote_webfinger,
@@ -22,17 +22,17 @@ def follow(request):
     """follow another user, here or abroad"""
     username = request.POST["user"]
     to_follow = get_user_from_username(request.user, username)
+    clear_cache(request.user, to_follow)
 
-    try:
-        models.UserFollowRequest.objects.create(
-            user_subject=request.user,
-            user_object=to_follow,
-        )
-    except IntegrityError:
-        pass
+    follow_request, created = models.UserFollowRequest.objects.get_or_create(
+        user_subject=request.user,
+        user_object=to_follow,
+    )
 
-    if request.GET.get("next"):
-        return redirect(request.GET.get("next", "/"))
+    if not created:
+        # this request probably failed to connect with the remote
+        # that means we should save to trigger a re-broadcast
+        follow_request.save()
 
     return redirect(to_follow.local_path)
 
@@ -49,14 +49,14 @@ def unfollow(request):
             user_subject=request.user, user_object=to_unfollow
         ).delete()
     except models.UserFollows.DoesNotExist:
-        pass
+        clear_cache(request.user, to_unfollow)
 
     try:
         models.UserFollowRequest.objects.get(
             user_subject=request.user, user_object=to_unfollow
         ).delete()
     except models.UserFollowRequest.DoesNotExist:
-        pass
+        clear_cache(request.user, to_unfollow)
 
     # this is handled with ajax so it shouldn't really matter
     return redirect(request.headers.get("Referer", "/"))


Test patch: