Repository: bookwyrm-social/bookwyrm

Issue:
status not showing up in my timeline
not sure if this is an issue, but a status didn't show up in my timeline.

This status showed up in my timeline:

<img width="1186" alt="yes" src="https://user-images.githubusercontent.com/63938514/130379395-3a2ee8cc-b4db-4be4-86e8-f01fc35aa0e6.png">

but not this one:

<img width="1195" alt="no" src="https://user-images.githubusercontent.com/63938514/130379412-4facf7a0-1837-4982-a93a-44407fe0d5ed.png">

it was the same on chrome and firefox, mac laptop and iphone.

i noticed this post on my oulipo mastadon instance on tusky. it was of particular interest because of my mild obsession with biblical history, so I went to read it on my laptop. but it was not on my timeline. it was on user's (mouse) profile page, which is were i got the image.



Discussion:

Resolving patch:
diff --git a/bookwyrm/activitystreams.py b/bookwyrm/activitystreams.py
--- a/bookwyrm/activitystreams.py
+++ b/bookwyrm/activitystreams.py
@@ -273,9 +273,10 @@ def add_status_on_create(sender, instance, created, *args, **kwargs):
         created_date__lt=instance.created_date,
     )
     for stream in streams.values():
-        stream.remove_object_from_related_stores(boosted)
+        audience = stream.get_stores_for_object(instance)
+        stream.remove_object_from_related_stores(boosted, stores=audience)
         for status in old_versions:
-            stream.remove_object_from_related_stores(status)
+            stream.remove_object_from_related_stores(status, stores=audience)
 
 
 @receiver(signals.post_delete, sender=models.Boost)
diff --git a/bookwyrm/redis_store.py b/bookwyrm/redis_store.py
--- a/bookwyrm/redis_store.py
+++ b/bookwyrm/redis_store.py
@@ -33,10 +33,11 @@ def add_object_to_related_stores(self, obj, execute=True):
         # and go!
         return pipeline.execute()
 
-    def remove_object_from_related_stores(self, obj):
+    def remove_object_from_related_stores(self, obj, stores=None):
         """remove an object from all stores"""
+        stores = stores or self.get_stores_for_object(obj)
         pipeline = r.pipeline()
-        for store in self.get_stores_for_object(obj):
+        for store in stores:
             pipeline.zrem(store, -1, obj.id)
         pipeline.execute()
 


Test patch:
diff --git a/bookwyrm/tests/test_activitystreams.py b/bookwyrm/tests/test_activitystreams.py
--- a/bookwyrm/tests/test_activitystreams.py
+++ b/bookwyrm/tests/test_activitystreams.py
@@ -8,6 +8,7 @@
 @patch("bookwyrm.activitystreams.ActivityStream.add_status")
 @patch("bookwyrm.activitystreams.BooksStream.add_book_statuses")
 @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
+# pylint: disable=too-many-public-methods
 class Activitystreams(TestCase):
     """using redis to build activity streams"""
 
@@ -286,3 +287,76 @@ def test_get_statuses_for_user_books(self, *_):
         # yes book, yes audience
         result = activitystreams.BooksStream().get_statuses_for_user(self.local_user)
         self.assertEqual(list(result), [status])
+
+    @patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores")
+    @patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores")
+    def test_boost_to_another_timeline(self, *_):
+        """add a boost and deduplicate the boosted status on the timeline"""
+        status = models.Status.objects.create(user=self.local_user, content="hi")
+        with patch(
+            "bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores"
+        ):
+            boost = models.Boost.objects.create(
+                boosted_status=status,
+                user=self.another_user,
+            )
+        with patch(
+            "bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores"
+        ) as mock:
+            activitystreams.add_status_on_create(models.Boost, boost, True)
+        self.assertTrue(mock.called)
+        call_args = mock.call_args
+        self.assertEqual(call_args[0][0], status)
+        self.assertEqual(
+            call_args[1]["stores"], ["{:d}-home".format(self.another_user.id)]
+        )
+
+    @patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores")
+    @patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores")
+    def test_boost_to_following_timeline(self, *_):
+        """add a boost and deduplicate the boosted status on the timeline"""
+        self.local_user.following.add(self.another_user)
+        status = models.Status.objects.create(user=self.local_user, content="hi")
+        with patch(
+            "bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores"
+        ):
+            boost = models.Boost.objects.create(
+                boosted_status=status,
+                user=self.another_user,
+            )
+        with patch(
+            "bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores"
+        ) as mock:
+            activitystreams.add_status_on_create(models.Boost, boost, True)
+        self.assertTrue(mock.called)
+        call_args = mock.call_args
+        self.assertEqual(call_args[0][0], status)
+        self.assertTrue(
+            "{:d}-home".format(self.another_user.id) in call_args[1]["stores"]
+        )
+        self.assertTrue(
+            "{:d}-home".format(self.local_user.id) in call_args[1]["stores"]
+        )
+
+    @patch("bookwyrm.activitystreams.LocalStream.remove_object_from_related_stores")
+    @patch("bookwyrm.activitystreams.BooksStream.remove_object_from_related_stores")
+    def test_boost_to_same_timeline(self, *_):
+        """add a boost and deduplicate the boosted status on the timeline"""
+        status = models.Status.objects.create(user=self.local_user, content="hi")
+        with patch(
+            "bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores"
+        ):
+            boost = models.Boost.objects.create(
+                boosted_status=status,
+                user=self.local_user,
+            )
+        with patch(
+            "bookwyrm.activitystreams.HomeStream.remove_object_from_related_stores"
+        ) as mock:
+            activitystreams.add_status_on_create(models.Boost, boost, True)
+        self.assertTrue(mock.called)
+        call_args = mock.call_args
+        self.assertEqual(call_args[0][0], status)
+        self.assertEqual(
+            call_args[1]["stores"], ["{:d}-home".format(self.local_user.id)]
+        )