From 958b4cfde916c9be71d7412fe1c90750ec578fdc Mon Sep 17 00:00:00 2001
From: William Pitcock <nenolod@dereferenced.org>
Date: Sun, 24 Mar 2019 23:45:57 +0000
Subject: [PATCH 01/12] migrations: add function to see if a thread can be
 satisfied

---
 ...4222404_add_thread_visibility_function.exs | 76 +++++++++++++++++++
 1 file changed, 76 insertions(+)
 create mode 100644 priv/repo/migrations/20190324222404_add_thread_visibility_function.exs

diff --git a/priv/repo/migrations/20190324222404_add_thread_visibility_function.exs b/priv/repo/migrations/20190324222404_add_thread_visibility_function.exs
new file mode 100644
index 000000000..cea0322e7
--- /dev/null
+++ b/priv/repo/migrations/20190324222404_add_thread_visibility_function.exs
@@ -0,0 +1,76 @@
+defmodule Pleroma.Repo.Migrations.AddThreadVisibilityFunction do
+  use Ecto.Migration
+  @disable_ddl_transaction true
+
+  def up do
+    statement = """
+    CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar) RETURNS boolean AS $$
+    DECLARE
+      public varchar := 'https://www.w3.org/ns/activitystreams#Public';
+      child objects%ROWTYPE;
+      activity activities%ROWTYPE;
+      actor_user users%ROWTYPE;
+      author users%ROWTYPE;
+      author_fa varchar;
+    BEGIN
+      --- Fetch our actor.
+      SELECT * INTO actor_user FROM users WHERE users.ap_id = actor;
+
+      --- Fetch our initial activity.
+      SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id;
+
+      LOOP
+        --- Ensure that we have an activity before continuing.
+        IF activity IS NULL THEN
+          RETURN true;
+        END IF;
+
+        --- Normalize the child object into child.
+        SELECT * INTO child FROM objects
+        INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
+        WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id';
+
+        --- Fetch the author.
+        SELECT * INTO author FROM users WHERE users.ap_id = activity.actor;
+
+        --- Prepare author's AS2 followers collection.
+        SELECT COALESCE(author.follower_address, '') INTO author_fa;
+
+        --- Check visibility.
+        IF activity.actor = actor THEN
+          --- activity visible
+          NULL;
+        ELSIF ARRAY[public] && activity.recipients THEN
+          --- activity visible
+          NULL;
+        ELSIF ARRAY[author_fa] && activity.recipients AND ARRAY[author_fa] && actor_user.following THEN
+          --- activity visible
+          NULL;
+        ELSIF ARRAY[actor] && activity.recipients THEN
+          --- activity visible
+          NULL;
+        ELSE
+          --- activity not visible, break out of the loop
+          RETURN false;
+        END IF;
+
+        --- If there's a parent, load it and do this all over again.
+        IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN
+          SELECT * INTO activity FROM activities
+          INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
+          WHERE child.data->>'inReplyTo' = objects.data->>'id';
+        ELSE
+          RETURN true;
+        END IF;
+      END LOOP;
+    END;
+    $$ LANGUAGE plpgsql IMMUTABLE;
+    """
+
+    execute(statement)
+  end
+
+  def down do
+    execute("drop function thread_visibility(actor varchar, activity_id varchar)")
+  end
+end

From 0387f5213805cdc4e0bf86f98797cefcd03ba61d Mon Sep 17 00:00:00 2001
From: William Pitcock <nenolod@dereferenced.org>
Date: Mon, 25 Mar 2019 00:06:02 +0000
Subject: [PATCH 02/12] activitypub: add restrict_thread_visibility()

---
 lib/pleroma/web/activity_pub/activity_pub.ex | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 233fee4fa..fec1bcd3e 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -569,6 +569,20 @@ defp restrict_visibility(_query, %{visibility: visibility})
 
   defp restrict_visibility(query, _visibility), do: query
 
+  defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}) do
+    query =
+      from(
+        a in query,
+        where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
+      )
+
+    Ecto.Adapters.SQL.to_sql(:all, Repo, query)
+
+    query
+  end
+
+  defp restrict_thread_visibility(query, _), do: query
+
   def fetch_user_activities(user, reading_user, params \\ %{}) do
     params =
       params
@@ -848,6 +862,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
     |> restrict_muted(opts)
     |> restrict_media(opts)
     |> restrict_visibility(opts)
+    |> restrict_thread_visibility(opts)
     |> restrict_replies(opts)
     |> restrict_reblogs(opts)
     |> restrict_pinned(opts)

From de114ffbb0f92d24fd370adaaf43ff301ab04b4b Mon Sep 17 00:00:00 2001
From: William Pitcock <nenolod@dereferenced.org>
Date: Mon, 25 Mar 2019 00:10:20 +0000
Subject: [PATCH 03/12] activitypub: remove contain_timeline()

---
 lib/pleroma/web/activity_pub/activity_pub.ex            | 8 --------
 lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 1 -
 lib/pleroma/web/twitter_api/twitter_api_controller.ex   | 4 +---
 3 files changed, 1 insertion(+), 12 deletions(-)

diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index fec1bcd3e..e544d0c50 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -980,12 +980,4 @@ def contain_broken_threads(%Activity{} = activity, %User{} = user) do
   def contain_activity(%Activity{} = activity, %User{} = user) do
     contain_broken_threads(activity, user)
   end
-
-  # do post-processing on a timeline
-  def contain_timeline(timeline, user) do
-    timeline
-    |> Enum.filter(fn activity ->
-      contain_activity(activity, user)
-    end)
-  end
 end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 87e597074..66056a846 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -303,7 +303,6 @@ def home_timeline(%{assigns: %{user: user}} = conn, params) do
     activities =
       [user.ap_id | user.following]
       |> ActivityPub.fetch_activities(params)
-      |> ActivityPub.contain_timeline(user)
       |> Enum.reverse()
 
     conn
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 3c5a70be9..31e86685a 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -101,9 +101,7 @@ def friends_timeline(%{assigns: %{user: user}} = conn, params) do
       |> Map.put("blocking_user", user)
       |> Map.put("user", user)
 
-    activities =
-      ActivityPub.fetch_activities([user.ap_id | user.following], params)
-      |> ActivityPub.contain_timeline(user)
+    activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
 
     conn
     |> put_view(ActivityView)

From 31db31c5879a2dedcc8dd4c671c4c9a79656355a Mon Sep 17 00:00:00 2001
From: William Pitcock <nenolod@dereferenced.org>
Date: Mon, 25 Mar 2019 00:38:28 +0000
Subject: [PATCH 04/12] activitypub: visibility: use SQL thread_visibility()
 function instead of manually walking the thread

---
 lib/pleroma/web/activity_pub/visibility.ex | 26 +++++++---------------
 1 file changed, 8 insertions(+), 18 deletions(-)

diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex
index b38ee0442..46dd46575 100644
--- a/lib/pleroma/web/activity_pub/visibility.ex
+++ b/lib/pleroma/web/activity_pub/visibility.ex
@@ -1,6 +1,7 @@
 defmodule Pleroma.Web.ActivityPub.Visibility do
   alias Pleroma.Activity
   alias Pleroma.Object
+  alias Pleroma.Repo
   alias Pleroma.User
 
   def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
@@ -38,25 +39,14 @@ def visible_for_user?(activity, user) do
     visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
   end
 
-  # guard
-  def entire_thread_visible_for_user?(nil, _user), do: false
+  def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
+    {:ok, %{rows: [[result]]}} =
+      Ecto.Adapters.SQL.query(Repo, "SELECT thread_visibility($1, $2)", [
+        user.ap_id,
+        activity.data["id"]
+      ])
 
-  # XXX: Probably even more inefficient than the previous implementation intended to be a placeholder untill https://git.pleroma.social/pleroma/pleroma/merge_requests/971 is in develop
-  # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
-
-  def entire_thread_visible_for_user?(
-        %Activity{} = tail,
-        # %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail,
-        user
-      ) do
-    case Object.normalize(tail) do
-      %{data: %{"inReplyTo" => parent_id}} when is_binary(parent_id) ->
-        parent = Activity.get_in_reply_to_activity(tail)
-        visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
-
-      _ ->
-        visible_for_user?(tail, user)
-    end
+    result
   end
 
   def get_visibility(object) do

From c7644313e72520a371e4bd417b1ff852365849b6 Mon Sep 17 00:00:00 2001
From: William Pitcock <nenolod@dereferenced.org>
Date: Mon, 25 Mar 2019 01:23:15 +0000
Subject: [PATCH 05/12] test: update obsolete test

---
 test/web/activity_pub/activity_pub_test.exs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index 0f90aa1ac..b41f6ab81 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -968,7 +968,8 @@ test "it filters broken threads" do
 
       assert length(activities) == 3
 
-      activities = ActivityPub.contain_timeline(activities, user1)
+      activities =
+        ActivityPub.fetch_activities([user1.ap_id | user1.following], %{"user" => user1})
 
       assert [public_activity, private_activity_1] == activities
       assert length(activities) == 2

From 75ce6adcffd2dbbc2ca2f83d7fe2d7fd659cd2f4 Mon Sep 17 00:00:00 2001
From: William Pitcock <nenolod@dereferenced.org>
Date: Mon, 25 Mar 2019 02:56:13 +0000
Subject: [PATCH 06/12] migration: only care about Create activities

---
 .../20190324222404_add_thread_visibility_function.exs        | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/priv/repo/migrations/20190324222404_add_thread_visibility_function.exs b/priv/repo/migrations/20190324222404_add_thread_visibility_function.exs
index cea0322e7..11aa47e83 100644
--- a/priv/repo/migrations/20190324222404_add_thread_visibility_function.exs
+++ b/priv/repo/migrations/20190324222404_add_thread_visibility_function.exs
@@ -25,6 +25,11 @@ def up do
           RETURN true;
         END IF;
 
+        --- We only care about Create activities.
+        IF activity.data->>'type' != 'Create' THEN
+          RETURN true;
+        END IF;
+
         --- Normalize the child object into child.
         SELECT * INTO child FROM objects
         INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'

From 0aada88b5594b6714b8d65f8bee9c325d77d6e7b Mon Sep 17 00:00:00 2001
From: William Pitcock <nenolod@dereferenced.org>
Date: Wed, 8 May 2019 23:17:51 +0000
Subject: [PATCH 07/12] bbs: chase timeline containment patch

---
 lib/pleroma/bbs/handler.ex | 1 -
 1 file changed, 1 deletion(-)

diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex
index 106fe5d18..f34be961f 100644
--- a/lib/pleroma/bbs/handler.ex
+++ b/lib/pleroma/bbs/handler.ex
@@ -95,7 +95,6 @@ def handle_command(state, "home") do
     activities =
       [user.ap_id | user.following]
       |> ActivityPub.fetch_activities(params)
-      |> ActivityPub.contain_timeline(user)
 
     Enum.each(activities, fn activity ->
       puts_activity(activity)

From 12f45e2a8907c74c6b65d866bc3bab547b31edfa Mon Sep 17 00:00:00 2001
From: William Pitcock <nenolod@dereferenced.org>
Date: Wed, 15 May 2019 16:22:52 +0000
Subject: [PATCH 08/12] update migration

---
 ...n.exs => 20190515222404_add_thread_visibility_function.exs} | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
 rename priv/repo/migrations/{20190324222404_add_thread_visibility_function.exs => 20190515222404_add_thread_visibility_function.exs} (97%)

diff --git a/priv/repo/migrations/20190324222404_add_thread_visibility_function.exs b/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs
similarity index 97%
rename from priv/repo/migrations/20190324222404_add_thread_visibility_function.exs
rename to priv/repo/migrations/20190515222404_add_thread_visibility_function.exs
index 11aa47e83..a3f717b89 100644
--- a/priv/repo/migrations/20190324222404_add_thread_visibility_function.exs
+++ b/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs
@@ -21,8 +21,9 @@ def up do
 
       LOOP
         --- Ensure that we have an activity before continuing.
+        --- If we don't, the thread is not satisfiable.
         IF activity IS NULL THEN
-          RETURN true;
+          RETURN false;
         END IF;
 
         --- We only care about Create activities.

From f09c3afdf51eea17103d1445b31b7a269c474538 Mon Sep 17 00:00:00 2001
From: William Pitcock <nenolod@dereferenced.org>
Date: Wed, 15 May 2019 16:23:01 +0000
Subject: [PATCH 09/12] chase test failures

---
 lib/pleroma/filter.ex                       | 3 ++-
 test/user_test.exs                          | 2 --
 test/web/activity_pub/activity_pub_test.exs | 9 ++++++---
 3 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index 79efc29f0..90457dadf 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -38,7 +38,8 @@ def get_filters(%User{id: user_id} = _user) do
     query =
       from(
         f in Pleroma.Filter,
-        where: f.user_id == ^user_id
+        where: f.user_id == ^user_id,
+        order_by: [desc: :id]
       )
 
     Repo.all(query)
diff --git a/test/user_test.exs b/test/user_test.exs
index 0b65e89e9..bb47b4958 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -873,7 +873,6 @@ test "hide a user's statuses from timelines and notifications" do
 
       assert [activity] ==
                ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
-               |> ActivityPub.contain_timeline(user2)
 
       {:ok, _user} = User.deactivate(user)
 
@@ -882,7 +881,6 @@ test "hide a user's statuses from timelines and notifications" do
 
       assert [] ==
                ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
-               |> ActivityPub.contain_timeline(user2)
     end
   end
 
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index b41f6ab81..34e23b852 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -960,18 +960,21 @@ test "it filters broken threads" do
           "in_reply_to_status_id" => private_activity_2.id
         })
 
-      activities = ActivityPub.fetch_activities([user1.ap_id | user1.following])
+      activities =
+        ActivityPub.fetch_activities([user1.ap_id | user1.following])
+        |> Enum.map(fn a -> a.id end)
 
       private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
 
-      assert [public_activity, private_activity_1, private_activity_3] == activities
+      assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities
 
       assert length(activities) == 3
 
       activities =
         ActivityPub.fetch_activities([user1.ap_id | user1.following], %{"user" => user1})
+        |> Enum.map(fn a -> a.id end)
 
-      assert [public_activity, private_activity_1] == activities
+      assert [public_activity.id, private_activity_1.id] == activities
       assert length(activities) == 2
     end
   end

From 71fa7eeb6fdc7cf2087a32fb515ad11b7bf90c01 Mon Sep 17 00:00:00 2001
From: William Pitcock <nenolod@dereferenced.org>
Date: Wed, 15 May 2019 16:54:14 +0000
Subject: [PATCH 10/12] thread visibility function: significantly improve
 efficiency

---
 ...5222404_add_thread_visibility_function.exs | 27 +++++++------------
 1 file changed, 9 insertions(+), 18 deletions(-)

diff --git a/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs b/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs
index a3f717b89..a4daf680b 100644
--- a/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs
+++ b/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs
@@ -10,8 +10,8 @@ def up do
       child objects%ROWTYPE;
       activity activities%ROWTYPE;
       actor_user users%ROWTYPE;
-      author users%ROWTYPE;
       author_fa varchar;
+      valid_recipients varchar[];
     BEGIN
       --- Fetch our actor.
       SELECT * INTO actor_user FROM users WHERE users.ap_id = actor;
@@ -36,26 +36,17 @@ def up do
         INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
         WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id';
 
-        --- Fetch the author.
-        SELECT * INTO author FROM users WHERE users.ap_id = activity.actor;
+        --- Fetch the author's AS2 following collection.
+        SELECT COALESCE(author.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor;
 
-        --- Prepare author's AS2 followers collection.
-        SELECT COALESCE(author.follower_address, '') INTO author_fa;
+        --- Prepare valid recipients array.
+        valid_recipients := ARRAY[actor, public];
+        IF ARRAY[author_fa] && actor_user.following THEN
+          valid_recipients := valid_recipients || author_fa;
+        END IF;
 
         --- Check visibility.
-        IF activity.actor = actor THEN
-          --- activity visible
-          NULL;
-        ELSIF ARRAY[public] && activity.recipients THEN
-          --- activity visible
-          NULL;
-        ELSIF ARRAY[author_fa] && activity.recipients AND ARRAY[author_fa] && actor_user.following THEN
-          --- activity visible
-          NULL;
-        ELSIF ARRAY[actor] && activity.recipients THEN
-          --- activity visible
-          NULL;
-        ELSE
+        IF NOT valid_recipients && activity.recipients THEN
           --- activity not visible, break out of the loop
           RETURN false;
         END IF;

From a591ab6112abdf162f4d6fdfbbcdd85bbaf75058 Mon Sep 17 00:00:00 2001
From: William Pitcock <nenolod@dereferenced.org>
Date: Wed, 15 May 2019 16:56:46 +0000
Subject: [PATCH 11/12] activity pub: remove Ecto SQL query dumps

---
 lib/pleroma/web/activity_pub/activity_pub.ex | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index e544d0c50..7cd5b889b 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -540,8 +540,6 @@ defp restrict_visibility(query, %{visibility: visibility})
             )
         )
 
-      Ecto.Adapters.SQL.to_sql(:all, Repo, query)
-
       query
     else
       Logger.error("Could not restrict visibility to #{visibility}")
@@ -557,8 +555,6 @@ defp restrict_visibility(query, %{visibility: visibility})
           fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
       )
 
-    Ecto.Adapters.SQL.to_sql(:all, Repo, query)
-
     query
   end
 
@@ -576,8 +572,6 @@ defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}) do
         where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
       )
 
-    Ecto.Adapters.SQL.to_sql(:all, Repo, query)
-
     query
   end
 

From f3971cbde3d69faec973717e1421f4a643ef947e Mon Sep 17 00:00:00 2001
From: William Pitcock <nenolod@dereferenced.org>
Date: Wed, 15 May 2019 17:02:40 +0000
Subject: [PATCH 12/12] thread visibility function: fix use of no longer used
 author variable

---
 .../20190515222404_add_thread_visibility_function.exs           | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs b/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs
index a4daf680b..dc9abc998 100644
--- a/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs
+++ b/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs
@@ -37,7 +37,7 @@ def up do
         WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id';
 
         --- Fetch the author's AS2 following collection.
-        SELECT COALESCE(author.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor;
+        SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor;
 
         --- Prepare valid recipients array.
         valid_recipients := ARRAY[actor, public];