reply filtering
[akkoma] / test / web / activity_pub / activity_pub_test.exs
index 9b7cfee631fe36c693cc75a99df7b8de7a9a4c01..8a1638a23133ce86f686cfa5c66c026bd25fb7e2 100644 (file)
@@ -1,5 +1,5 @@
 # Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
   alias Pleroma.Activity
   alias Pleroma.Builders.ActivityBuilder
+  alias Pleroma.Config
   alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.User
@@ -15,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.AdminAPI.AccountView
   alias Pleroma.Web.CommonAPI
+  alias Pleroma.Web.Federator
 
   import Pleroma.Factory
   import Tesla.Mock
@@ -25,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     :ok
   end
 
-  clear_config([:instance, :federating])
+  setup do: clear_config([:instance, :federating])
 
   describe "streaming out participations" do
     test "it streams them out" do
@@ -178,7 +180,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
       assert user.ap_id == user_id
       assert user.nickname == "admin@mastodon.example.org"
-      assert user.source_data
       assert user.ap_enabled
       assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
     end
@@ -224,7 +225,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
   describe "insertion" do
     test "drops activities beyond a certain limit" do
-      limit = Pleroma.Config.get([:instance, :remote_limit])
+      limit = Config.get([:instance, :remote_limit])
 
       random_text =
         :crypto.strong_rand_bytes(limit + 1)
@@ -385,6 +386,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
   end
 
   describe "create activities" do
+    test "it reverts create" do
+      user = insert(:user)
+
+      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+        assert {:error, :reverted} =
+                 ActivityPub.create(%{
+                   to: ["user1", "user2"],
+                   actor: user,
+                   context: "",
+                   object: %{
+                     "to" => ["user1", "user2"],
+                     "type" => "Note",
+                     "content" => "testing"
+                   }
+                 })
+      end
+
+      assert Repo.aggregate(Activity, :count, :id) == 0
+      assert Repo.aggregate(Object, :count, :id) == 0
+    end
+
     test "removes doubled 'to' recipients" do
       user = insert(:user)
 
@@ -852,8 +874,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
   end
 
   describe "react to an object" do
-    test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
-      Pleroma.Config.put([:instance, :federating], true)
+    test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
+      Config.put([:instance, :federating], true)
       user = insert(:user)
       reactor = insert(:user)
       {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
@@ -861,7 +883,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
 
-      assert called(Pleroma.Web.Federator.publish(reaction_activity))
+      assert called(Federator.publish(reaction_activity))
     end
 
     test "adds an emoji reaction activity to the db" do
@@ -899,11 +921,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
                ["☕", [third_user.ap_id]]
              ]
     end
+
+    test "reverts emoji reaction on error" do
+      [user, reactor] = insert_list(2, :user)
+
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
+      object = Object.normalize(activity)
+
+      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+        assert {:error, :reverted} = ActivityPub.react_with_emoji(reactor, object, "😀")
+      end
+
+      object = Object.get_by_ap_id(object.data["id"])
+      refute object.data["reaction_count"]
+      refute object.data["reactions"]
+    end
   end
 
   describe "unreacting to an object" do
-    test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
-      Pleroma.Config.put([:instance, :federating], true)
+    test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
+      Config.put([:instance, :federating], true)
       user = insert(:user)
       reactor = insert(:user)
       {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
@@ -911,12 +948,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
 
-      assert called(Pleroma.Web.Federator.publish(reaction_activity))
+      assert called(Federator.publish(reaction_activity))
 
       {:ok, unreaction_activity, _object} =
         ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
 
-      assert called(Pleroma.Web.Federator.publish(unreaction_activity))
+      assert called(Federator.publish(unreaction_activity))
     end
 
     test "adds an undo activity to the db" do
@@ -937,79 +974,62 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       assert object.data["reaction_count"] == 0
       assert object.data["reactions"] == []
     end
-  end
 
-  describe "like an object" do
-    test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
-      Pleroma.Config.put([:instance, :federating], true)
-      note_activity = insert(:note_activity)
-      assert object_activity = Object.normalize(note_activity)
+    test "reverts emoji unreact on error" do
+      [user, reactor] = insert_list(2, :user)
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
+      object = Object.normalize(activity)
 
-      user = insert(:user)
+      {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "😀")
 
-      {:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
-      assert called(Pleroma.Web.Federator.publish(like_activity))
-    end
-
-    test "returns exist activity if object already liked" do
-      note_activity = insert(:note_activity)
-      assert object_activity = Object.normalize(note_activity)
-
-      user = insert(:user)
+      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+        assert {:error, :reverted} =
+                 ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
+      end
 
-      {:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
+      object = Object.get_by_ap_id(object.data["id"])
 
-      {:ok, like_activity_exist, _object} = ActivityPub.like(user, object_activity)
-      assert like_activity == like_activity_exist
+      assert object.data["reaction_count"] == 1
+      assert object.data["reactions"] == [["😀", [reactor.ap_id]]]
     end
+  end
 
-    test "adds a like activity to the db" do
-      note_activity = insert(:note_activity)
-      assert object = Object.normalize(note_activity)
+  describe "unliking" do
+    test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
+      Config.put([:instance, :federating], true)
 
+      note_activity = insert(:note_activity)
+      object = Object.normalize(note_activity)
       user = insert(:user)
-      user_two = insert(:user)
 
-      {:ok, like_activity, object} = ActivityPub.like(user, object)
+      {:ok, object} = ActivityPub.unlike(user, object)
+      refute called(Federator.publish())
 
-      assert like_activity.data["actor"] == user.ap_id
-      assert like_activity.data["type"] == "Like"
-      assert like_activity.data["object"] == object.data["id"]
-      assert like_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
-      assert like_activity.data["context"] == object.data["context"]
+      {:ok, _like_activity} = CommonAPI.favorite(user, note_activity.id)
+      object = Object.get_by_id(object.id)
       assert object.data["like_count"] == 1
-      assert object.data["likes"] == [user.ap_id]
 
-      # Just return the original activity if the user already liked it.
-      {:ok, same_like_activity, object} = ActivityPub.like(user, object)
-
-      assert like_activity == same_like_activity
-      assert object.data["likes"] == [user.ap_id]
-      assert object.data["like_count"] == 1
+      {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
+      assert object.data["like_count"] == 0
 
-      {:ok, _like_activity, object} = ActivityPub.like(user_two, object)
-      assert object.data["like_count"] == 2
+      assert called(Federator.publish(unlike_activity))
     end
-  end
-
-  describe "unliking" do
-    test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
-      Pleroma.Config.put([:instance, :federating], true)
 
+    test "reverts unliking on error" do
       note_activity = insert(:note_activity)
-      object = Object.normalize(note_activity)
       user = insert(:user)
 
-      {:ok, object} = ActivityPub.unlike(user, object)
-      refute called(Pleroma.Web.Federator.publish())
-
-      {:ok, _like_activity, object} = ActivityPub.like(user, object)
+      {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
+      object = Object.normalize(note_activity)
       assert object.data["like_count"] == 1
 
-      {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
-      assert object.data["like_count"] == 0
+      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+        assert {:error, :reverted} = ActivityPub.unlike(user, object)
+      end
 
-      assert called(Pleroma.Web.Federator.publish(unlike_activity))
+      assert Object.get_by_ap_id(object.data["id"]) == object
+      assert object.data["like_count"] == 1
+      assert Activity.get_by_id(like_activity.id)
     end
 
     test "unliking a previously liked object" do
@@ -1021,7 +1041,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       {:ok, object} = ActivityPub.unlike(user, object)
       assert object.data["like_count"] == 0
 
-      {:ok, like_activity, object} = ActivityPub.like(user, object)
+      {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
+
+      object = Object.get_by_id(object.id)
       assert object.data["like_count"] == 1
 
       {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
@@ -1051,6 +1073,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       assert announce_activity.data["actor"] == user.ap_id
       assert announce_activity.data["context"] == object.data["context"]
     end
+
+    test "reverts annouce from object on error" do
+      note_activity = insert(:note_activity)
+      object = Object.normalize(note_activity)
+      user = insert(:user)
+
+      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+        assert {:error, :reverted} = ActivityPub.announce(user, object)
+      end
+
+      reloaded_object = Object.get_by_ap_id(object.data["id"])
+      assert reloaded_object == object
+      refute reloaded_object.data["announcement_count"]
+      refute reloaded_object.data["announcements"]
+    end
   end
 
   describe "announcing a private object" do
@@ -1093,8 +1130,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       user = insert(:user)
 
       # Unannouncing an object that is not announced does nothing
-      {:ok, object} = ActivityPub.unannounce(user, object)
-      # assert object.data["announcement_count"] == 0
+      {:ok, object} = ActivityPub.unannounce(user, object)
+      refute object.data["announcement_count"]
 
       {:ok, announce_activity, object} = ActivityPub.announce(user, object)
       assert object.data["announcement_count"] == 1
@@ -1114,6 +1151,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       assert Activity.get_by_id(announce_activity.id) == nil
     end
+
+    test "reverts unannouncing on error" do
+      note_activity = insert(:note_activity)
+      object = Object.normalize(note_activity)
+      user = insert(:user)
+
+      {:ok, _announce_activity, object} = ActivityPub.announce(user, object)
+      assert object.data["announcement_count"] == 1
+
+      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+        assert {:error, :reverted} = ActivityPub.unannounce(user, object)
+      end
+
+      object = Object.get_by_ap_id(object.data["id"])
+      assert object.data["announcement_count"] == 1
+    end
   end
 
   describe "uploading files" do
@@ -1148,6 +1201,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
   end
 
   describe "following / unfollowing" do
+    test "it reverts follow activity" do
+      follower = insert(:user)
+      followed = insert(:user)
+
+      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+        assert {:error, :reverted} = ActivityPub.follow(follower, followed)
+      end
+
+      assert Repo.aggregate(Activity, :count, :id) == 0
+      assert Repo.aggregate(Object, :count, :id) == 0
+    end
+
+    test "it reverts unfollow activity" do
+      follower = insert(:user)
+      followed = insert(:user)
+
+      {:ok, follow_activity} = ActivityPub.follow(follower, followed)
+
+      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+        assert {:error, :reverted} = ActivityPub.unfollow(follower, followed)
+      end
+
+      activity = Activity.get_by_id(follow_activity.id)
+      assert activity.data["type"] == "Follow"
+      assert activity.data["actor"] == follower.ap_id
+
+      assert activity.data["object"] == followed.ap_id
+    end
+
     test "creates a follow activity" do
       follower = insert(:user)
       followed = insert(:user)
@@ -1194,6 +1276,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
   end
 
   describe "blocking / unblocking" do
+    test "reverts block activity on error" do
+      [blocker, blocked] = insert_list(2, :user)
+
+      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+        assert {:error, :reverted} = ActivityPub.block(blocker, blocked)
+      end
+
+      assert Repo.aggregate(Activity, :count, :id) == 0
+      assert Repo.aggregate(Object, :count, :id) == 0
+    end
+
     test "creates a block activity" do
       blocker = insert(:user)
       blocked = insert(:user)
@@ -1205,6 +1298,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       assert activity.data["object"] == blocked.ap_id
     end
 
+    test "reverts unblock activity on error" do
+      [blocker, blocked] = insert_list(2, :user)
+      {:ok, block_activity} = ActivityPub.block(blocker, blocked)
+
+      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+        assert {:error, :reverted} = ActivityPub.unblock(blocker, blocked)
+      end
+
+      assert block_activity.data["type"] == "Block"
+      assert block_activity.data["actor"] == blocker.ap_id
+
+      assert Repo.aggregate(Activity, :count, :id) == 1
+      assert Repo.aggregate(Object, :count, :id) == 1
+    end
+
     test "creates an undo activity for the last block" do
       blocker = insert(:user)
       blocked = insert(:user)
@@ -1224,7 +1332,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
   end
 
   describe "deletion" do
-    clear_config([:instance, :rewrite_policy])
+    setup do: clear_config([:instance, :rewrite_policy])
+
+    test "it reverts deletion on error" do
+      note = insert(:note_activity)
+      object = Object.normalize(note)
+
+      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
+        assert {:error, :reverted} = ActivityPub.delete(object)
+      end
+
+      assert Repo.aggregate(Activity, :count, :id) == 1
+      assert Repo.get(Object, object.id) == object
+      assert Activity.get_by_id(note.id) == note
+    end
 
     test "it creates a delete activity and deletes the original object" do
       note = insert(:note_activity)
@@ -1240,6 +1361,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       assert Repo.get(Object, object.id).data["type"] == "Tombstone"
     end
 
+    test "it doesn't fail when an activity was already deleted" do
+      {:ok, delete} = insert(:note_activity) |> Object.normalize() |> ActivityPub.delete()
+
+      assert {:ok, ^delete} = delete |> Object.normalize() |> ActivityPub.delete()
+    end
+
     test "decrements user note count only for public activities" do
       user = insert(:user, note_count: 10)
 
@@ -1395,7 +1522,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
   end
 
   describe "update" do
-    clear_config([:instance, :max_pinned_statuses])
+    setup do: clear_config([:instance, :max_pinned_statuses])
 
     test "it creates an update activity with the new user data" do
       user = insert(:user)
@@ -1419,7 +1546,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
   end
 
   test "returned pinned statuses" do
-    Pleroma.Config.put([:instance, :max_pinned_statuses], 3)
+    Config.put([:instance, :max_pinned_statuses], 3)
     user = insert(:user)
 
     {:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
@@ -1709,14 +1836,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       {:ok, a4} = CommonAPI.post(user2, %{"status" => "Agent Smith "})
       {:ok, a5} = CommonAPI.post(user1, %{"status" => "Red or Blue "})
 
-      {:ok, _, _} = CommonAPI.favorite(a4.id, user)
-      {:ok, _, _} = CommonAPI.favorite(a3.id, other_user)
-      {:ok, _, _} = CommonAPI.favorite(a3.id, user)
-      {:ok, _, _} = CommonAPI.favorite(a5.id, other_user)
-      {:ok, _, _} = CommonAPI.favorite(a5.id, user)
-      {:ok, _, _} = CommonAPI.favorite(a4.id, other_user)
-      {:ok, _, _} = CommonAPI.favorite(a1.id, user)
-      {:ok, _, _} = CommonAPI.favorite(a1.id, other_user)
+      {:ok, _} = CommonAPI.favorite(user, a4.id)
+      {:ok, _} = CommonAPI.favorite(other_user, a3.id)
+      {:ok, _} = CommonAPI.favorite(user, a3.id)
+      {:ok, _} = CommonAPI.favorite(other_user, a5.id)
+      {:ok, _} = CommonAPI.favorite(user, a5.id)
+      {:ok, _} = CommonAPI.favorite(other_user, a4.id)
+      {:ok, _} = CommonAPI.favorite(user, a1.id)
+      {:ok, _} = CommonAPI.favorite(other_user, a1.id)
       result = ActivityPub.fetch_favourites(user)
 
       assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id]
@@ -1770,11 +1897,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 
       activity = %Activity{activity | object: nil}
 
-      assert [%Notification{activity: ^activity}] =
-               Notification.for_user(follower, %{with_move: true})
+      assert [%Notification{activity: ^activity}] = Notification.for_user(follower)
 
-      assert [%Notification{activity: ^activity}] =
-               Notification.for_user(follower_move_opted_out, %{with_move: true})
+      assert [%Notification{activity: ^activity}] = Notification.for_user(follower_move_opted_out)
     end
 
     test "old user must be in the new user's `also_known_as` list" do
@@ -1785,4 +1910,490 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
                ActivityPub.move(old_user, new_user)
     end
   end
+
+  test "doesn't retrieve replies activities with exclude_replies" do
+    user = insert(:user)
+
+    {:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"})
+
+    {:ok, _reply} =
+      CommonAPI.post(user, %{"status" => "yeah", "in_reply_to_status_id" => activity.id})
+
+    [result] = ActivityPub.fetch_public_activities(%{"exclude_replies" => "true"})
+
+    assert result.id == activity.id
+
+    assert length(ActivityPub.fetch_public_activities()) == 2
+  end
+
+  describe "replies filtering with public messages" do
+    setup :public_messages
+
+    test "public timeline", %{users: %{u1: user}} do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 16
+    end
+
+    test "public timeline with reply_visibility `following`", %{
+      users: %{u1: user},
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4,
+      activities: activities
+    } do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_visibility", "following")
+        |> Map.put("user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 14
+
+      visible_ids =
+        Map.values(u1) ++ Map.values(u2) ++ Map.values(u4) ++ Map.values(activities) ++ [u3[:r1]]
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+
+    test "public timeline with reply_visibility `self`", %{
+      users: %{u1: user},
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4,
+      activities: activities
+    } do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_visibility", "self")
+        |> Map.put("user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 10
+      visible_ids = Map.values(u1) ++ [u2[:r1], u3[:r1], u4[:r1]] ++ Map.values(activities)
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+
+    test "home timeline", %{
+      users: %{u1: user},
+      activities: activities,
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4
+    } do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 13
+
+      visible_ids =
+        Map.values(u1) ++
+          Map.values(u3) ++
+          [
+            activities[:a1],
+            activities[:a2],
+            activities[:a4],
+            u2[:r1],
+            u2[:r3],
+            u4[:r1],
+            u4[:r2]
+          ]
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+
+    test "home timeline with reply_visibility `following`", %{
+      users: %{u1: user},
+      activities: activities,
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4
+    } do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_visibility", "following")
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 11
+
+      visible_ids =
+        Map.values(u1) ++
+          [
+            activities[:a1],
+            activities[:a2],
+            activities[:a4],
+            u2[:r1],
+            u2[:r3],
+            u3[:r1],
+            u4[:r1],
+            u4[:r2]
+          ]
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+
+    test "home timeline with reply_visibility `self`", %{
+      users: %{u1: user},
+      activities: activities,
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4
+    } do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_visibility", "self")
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 9
+
+      visible_ids =
+        Map.values(u1) ++
+          [
+            activities[:a1],
+            activities[:a2],
+            activities[:a4],
+            u2[:r1],
+            u3[:r1],
+            u4[:r1]
+          ]
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+  end
+
+  describe "replies filtering with private messages" do
+    setup :private_messages
+
+    test "public timeline", %{users: %{u1: user}} do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert activities_ids == []
+    end
+
+    test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_visibility", "following")
+        |> Map.put("user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert activities_ids == []
+    end
+
+    test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do
+      activities_ids =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("local_only", false)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("reply_visibility", "self")
+        |> Map.put("user", user)
+        |> ActivityPub.fetch_public_activities()
+        |> Enum.map(& &1.id)
+
+      assert activities_ids == []
+    end
+
+    test "home timeline", %{users: %{u1: user}} do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 12
+    end
+
+    test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_visibility", "following")
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 12
+    end
+
+    test "home timeline with default reply_visibility `self`", %{
+      users: %{u1: user},
+      activities: activities,
+      u1: u1,
+      u2: u2,
+      u3: u3,
+      u4: u4
+    } do
+      params =
+        %{}
+        |> Map.put("type", ["Create", "Announce"])
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+        |> Map.put("reply_visibility", "self")
+
+      activities_ids =
+        ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
+        |> Enum.map(& &1.id)
+
+      assert length(activities_ids) == 10
+
+      visible_ids =
+        Map.values(u1) ++ Map.values(u4) ++ [u2[:r1], u3[:r1]] ++ Map.values(activities)
+
+      assert Enum.all?(visible_ids, &(&1 in activities_ids))
+    end
+  end
+
+  defp public_messages(_) do
+    [u1, u2, u3, u4] = insert_list(4, :user)
+    {:ok, u1} = User.follow(u1, u2)
+    {:ok, u2} = User.follow(u2, u1)
+    {:ok, u1} = User.follow(u1, u4)
+    {:ok, u4} = User.follow(u4, u1)
+
+    {:ok, u2} = User.follow(u2, u3)
+    {:ok, u3} = User.follow(u3, u2)
+
+    {:ok, a1} = CommonAPI.post(u1, %{"status" => "Status"})
+
+    {:ok, r1_1} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u1.nickname} reply from u2 to u1",
+        "in_reply_to_status_id" => a1.id
+      })
+
+    {:ok, r1_2} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u1.nickname} reply from u3 to u1",
+        "in_reply_to_status_id" => a1.id
+      })
+
+    {:ok, r1_3} =
+      CommonAPI.post(u4, %{
+        "status" => "@#{u1.nickname} reply from u4 to u1",
+        "in_reply_to_status_id" => a1.id
+      })
+
+    {:ok, a2} = CommonAPI.post(u2, %{"status" => "Status"})
+
+    {:ok, r2_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u2.nickname} reply from u1 to u2",
+        "in_reply_to_status_id" => a2.id
+      })
+
+    {:ok, r2_2} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u2.nickname} reply from u3 to u2",
+        "in_reply_to_status_id" => a2.id
+      })
+
+    {:ok, r2_3} =
+      CommonAPI.post(u4, %{
+        "status" => "@#{u2.nickname} reply from u4 to u2",
+        "in_reply_to_status_id" => a2.id
+      })
+
+    {:ok, a3} = CommonAPI.post(u3, %{"status" => "Status"})
+
+    {:ok, r3_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u3.nickname} reply from u1 to u3",
+        "in_reply_to_status_id" => a3.id
+      })
+
+    {:ok, r3_2} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u3.nickname} reply from u2 to u3",
+        "in_reply_to_status_id" => a3.id
+      })
+
+    {:ok, r3_3} =
+      CommonAPI.post(u4, %{
+        "status" => "@#{u3.nickname} reply from u4 to u3",
+        "in_reply_to_status_id" => a3.id
+      })
+
+    {:ok, a4} = CommonAPI.post(u4, %{"status" => "Status"})
+
+    {:ok, r4_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u4.nickname} reply from u1 to u4",
+        "in_reply_to_status_id" => a4.id
+      })
+
+    {:ok, r4_2} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u4.nickname} reply from u2 to u4",
+        "in_reply_to_status_id" => a4.id
+      })
+
+    {:ok, r4_3} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u4.nickname} reply from u3 to u4",
+        "in_reply_to_status_id" => a4.id
+      })
+
+    {:ok,
+     users: %{u1: u1, u2: u2, u3: u3, u4: u4},
+     activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
+     u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
+     u2: %{r1: r2_1.id, r2: r2_2.id, r3: r2_3.id},
+     u3: %{r1: r3_1.id, r2: r3_2.id, r3: r3_3.id},
+     u4: %{r1: r4_1.id, r2: r4_2.id, r3: r4_3.id}}
+  end
+
+  defp private_messages(_) do
+    [u1, u2, u3, u4] = insert_list(4, :user)
+    {:ok, u1} = User.follow(u1, u2)
+    {:ok, u2} = User.follow(u2, u1)
+    {:ok, u1} = User.follow(u1, u3)
+    {:ok, u3} = User.follow(u3, u1)
+    {:ok, u1} = User.follow(u1, u4)
+    {:ok, u4} = User.follow(u4, u1)
+
+    {:ok, u2} = User.follow(u2, u3)
+    {:ok, u3} = User.follow(u3, u2)
+
+    {:ok, a1} = CommonAPI.post(u1, %{"status" => "Status", "visibility" => "private"})
+
+    {:ok, r1_1} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u1.nickname} reply from u2 to u1",
+        "in_reply_to_status_id" => a1.id,
+        "visibility" => "private"
+      })
+
+    {:ok, r1_2} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u1.nickname} reply from u3 to u1",
+        "in_reply_to_status_id" => a1.id,
+        "visibility" => "private"
+      })
+
+    {:ok, r1_3} =
+      CommonAPI.post(u4, %{
+        "status" => "@#{u1.nickname} reply from u4 to u1",
+        "in_reply_to_status_id" => a1.id,
+        "visibility" => "private"
+      })
+
+    {:ok, a2} = CommonAPI.post(u2, %{"status" => "Status", "visibility" => "private"})
+
+    {:ok, r2_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u2.nickname} reply from u1 to u2",
+        "in_reply_to_status_id" => a2.id,
+        "visibility" => "private"
+      })
+
+    {:ok, r2_2} =
+      CommonAPI.post(u3, %{
+        "status" => "@#{u2.nickname} reply from u3 to u2",
+        "in_reply_to_status_id" => a2.id,
+        "visibility" => "private"
+      })
+
+    {:ok, a3} = CommonAPI.post(u3, %{"status" => "Status", "visibility" => "private"})
+
+    {:ok, r3_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u3.nickname} reply from u1 to u3",
+        "in_reply_to_status_id" => a3.id,
+        "visibility" => "private"
+      })
+
+    {:ok, r3_2} =
+      CommonAPI.post(u2, %{
+        "status" => "@#{u3.nickname} reply from u2 to u3",
+        "in_reply_to_status_id" => a3.id,
+        "visibility" => "private"
+      })
+
+    {:ok, a4} = CommonAPI.post(u4, %{"status" => "Status", "visibility" => "private"})
+
+    {:ok, r4_1} =
+      CommonAPI.post(u1, %{
+        "status" => "@#{u4.nickname} reply from u1 to u4",
+        "in_reply_to_status_id" => a4.id,
+        "visibility" => "private"
+      })
+
+    {:ok,
+     users: %{u1: u1, u2: u2, u3: u3, u4: u4},
+     activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
+     u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
+     u2: %{r1: r2_1.id, r2: r2_2.id},
+     u3: %{r1: r3_1.id, r2: r3_2.id},
+     u4: %{r1: r4_1.id}}
+  end
 end