Repository: bookwyrm-social/bookwyrm

Issue:
Users from blocked federated instances still show up in "Who to follow" Home widget
**Describe the bug**
Apologies if what I'm doing is against fediverse etiquette first of all... I got a personal bookwyrm instance stood up for my house, and decided to block the default bookwyrm.social federation so we could use the software and be on our own island for now as we play with the software. However, the users that were automatically selected for "Who to follow" remain on our home page, even though you can't access those accounts anymore. Expectation is that only valid active users would remain here.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to Home Timeline and scroll down to see "Who to follow"
2. Note the instances those users are on
2. Go to the Admin Portal for an instance
3. Block the instance those users are on
4. Go back to the Home page

**Expected behavior**
The "Who to follow" widget shows only users that are accessible to the instance. Any changes to federated instance status would have relevant widgets refresh at the appropriate time.

**Screenshots**
![image](https://user-images.githubusercontent.com/934801/132164898-e7e059fb-971f-4032-bbdf-9612b80e7326.png)

![image](https://user-images.githubusercontent.com/934801/132164868-0cc744ef-e7ac-416f-8061-8ef23536909e.png)


**Instance**
My local bookwyrm instance

**Additional context**
I tried to delete these users using the admin dashboard, but that threw a lot of users that it couldn't delete the users without deleting a bunch of other objects, so I don't think that was the right approach and didn't investigate further.

---

**Desktop (please complete the following information):**
    - OS: Pop OS 21.04
    - Browser: Firefox
    - Version: 92


Discussion:
Also just noticed that when I block the bookwyrm.social federated instance, it also is impacting the book import ability, specifically no covers showed up for any of the books that were imported, even though Open Library and Inventaire both have covers for the editions imported.
Actually, this may have been because some of the Celery jobs were failing? I saw in one of the logs that some jobs were failing to update. Unfortunately by me attempting to fix it by exposing and changing the ports used in the env file, I broke Redis connection auth and can't get it working again so I have to figure out how to fix that tomorrow... Similar failure as to #1237 
Whew thank you! I accounted for users blocking one another and for individual deactivation, which in theory ought to account for domain-level blocks. I'll take a look and see if it's a task issue or something I missed in the implementation

Resolving patch:
diff --git a/bookwyrm/models/federated_server.py b/bookwyrm/models/federated_server.py
--- a/bookwyrm/models/federated_server.py
+++ b/bookwyrm/models/federated_server.py
@@ -28,7 +28,7 @@ class FederatedServer(BookWyrmModel):
     def block(self):
         """block a server"""
         self.status = "blocked"
-        self.save()
+        self.save(update_fields=["status"])
 
         # deactivate all associated users
         self.user_set.filter(is_active=True).update(
@@ -45,7 +45,7 @@ def block(self):
     def unblock(self):
         """unblock a server"""
         self.status = "federated"
-        self.save()
+        self.save(update_fields=["status"])
 
         self.user_set.filter(deactivation_reason="domain_block").update(
             is_active=True, deactivation_reason=None
diff --git a/bookwyrm/suggested_users.py b/bookwyrm/suggested_users.py
--- a/bookwyrm/suggested_users.py
+++ b/bookwyrm/suggested_users.py
@@ -86,10 +86,12 @@ def get_suggestions(self, user):
         values = self.get_store(self.store_id(user), withscores=True)
         results = []
         # annotate users with mutuals and shared book counts
-        for user_id, rank in values[:5]:
+        for user_id, rank in values:
             counts = self.get_counts_from_rank(rank)
             try:
-                user = models.User.objects.get(id=user_id)
+                user = models.User.objects.get(
+                    id=user_id, is_active=True, bookwyrm_user=True
+                )
             except models.User.DoesNotExist as err:
                 # if this happens, the suggestions are janked way up
                 logger.exception(err)
@@ -97,6 +99,8 @@ def get_suggestions(self, user):
             user.mutuals = counts["mutuals"]
             # user.shared_books = counts["shared_books"]
             results.append(user)
+            if len(results) >= 5:
+                break
         return results
 
 
@@ -178,13 +182,21 @@ def update_suggestions_on_unfollow(sender, instance, **kwargs):
 
 @receiver(signals.post_save, sender=models.User)
 # pylint: disable=unused-argument, too-many-arguments
-def add_new_user(sender, instance, created, update_fields=None, **kwargs):
-    """a new user, wow how cool"""
+def update_user(sender, instance, created, update_fields=None, **kwargs):
+    """an updated user, neat"""
     # a new user is found, create suggestions for them
     if created and instance.local:
         rerank_suggestions_task.delay(instance.id)
 
-    if update_fields and not "discoverable" in update_fields:
+    # we know what fields were updated and discoverability didn't change
+    if not instance.bookwyrm_user or (
+        update_fields and not "discoverable" in update_fields
+    ):
+        return
+
+    # deleted the user
+    if not created and not instance.is_active:
+        remove_user_task.delay(instance.id)
         return
 
     # this happens on every save, not just when discoverability changes, annoyingly
@@ -194,6 +206,25 @@ def add_new_user(sender, instance, created, update_fields=None, **kwargs):
         remove_user_task.delay(instance.id)
 
 
+@receiver(signals.post_save, sender=models.FederatedServer)
+def domain_level_update(sender, instance, created, update_fields=None, **kwargs):
+    """remove users on a domain block"""
+    if (
+        not update_fields
+        or "status" not in update_fields
+        or instance.application_type != "bookwyrm"
+    ):
+        return
+
+    if instance.status == "blocked":
+        bulk_remove_instance_task.delay(instance.id)
+        return
+    bulk_add_instance_task.delay(instance.id)
+
+
+# ------------------- TASKS
+
+
 @app.task(queue="low_priority")
 def rerank_suggestions_task(user_id):
     """do the hard work in celery"""
@@ -219,3 +250,17 @@ def remove_suggestion_task(user_id, suggested_user_id):
     """remove a specific user from a specific user's suggestions"""
     suggested_user = models.User.objects.get(id=suggested_user_id)
     suggested_users.remove_suggestion(user_id, suggested_user)
+
+
+@app.task(queue="low_priority")
+def bulk_remove_instance_task(instance_id):
+    """remove a bunch of users from recs"""
+    for user in models.User.objects.filter(federated_server__id=instance_id):
+        suggested_users.remove_object_from_related_stores(user)
+
+
+@app.task(queue="low_priority")
+def bulk_add_instance_task(instance_id):
+    """remove a bunch of users from recs"""
+    for user in models.User.objects.filter(federated_server__id=instance_id):
+        suggested_users.rerank_obj(user, update_only=False)


Test patch:
diff --git a/bookwyrm/tests/views/test_federation.py b/bookwyrm/tests/views/test_federation.py
--- a/bookwyrm/tests/views/test_federation.py
+++ b/bookwyrm/tests/views/test_federation.py
@@ -80,7 +80,9 @@ def test_server_page_block(self):
         request.user = self.local_user
         request.user.is_superuser = True
 
-        view(request, server.id)
+        with patch("bookwyrm.suggested_users.bulk_remove_instance_task.delay") as mock:
+            view(request, server.id)
+        self.assertEqual(mock.call_count, 1)
 
         server.refresh_from_db()
         self.remote_user.refresh_from_db()
@@ -118,7 +120,11 @@ def test_server_page_unblock(self):
         request.user = self.local_user
         request.user.is_superuser = True
 
-        views.federation.unblock_server(request, server.id)
+        with patch("bookwyrm.suggested_users.bulk_add_instance_task.delay") as mock:
+            views.federation.unblock_server(request, server.id)
+        self.assertEqual(mock.call_count, 1)
+        self.assertEqual(mock.call_args[0][0], server.id)
+
         server.refresh_from_db()
         self.remote_user.refresh_from_db()
         self.assertEqual(server.status, "federated")