[#1505] Added Mastodon-compatible `replies` collection to Note federated representation.
authorIvan Tashkinov <ivantashkinov@gmail.com>
Wed, 22 Jan 2020 18:10:17 +0000 (21:10 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Wed, 22 Jan 2020 18:10:17 +0000 (21:10 +0300)
config/config.exs
config/description.exs
lib/pleroma/activity.ex
lib/pleroma/activity/queries.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
test/web/activity_pub/transmogrifier_test.exs

index 2c154eb456e30475a1a5995759648519324b269f..60642c467489d7eecd0be5c9f5a38d18222522c8 100644 (file)
@@ -620,6 +620,10 @@ config :pleroma, :modules, runtime_dir: "instance/modules"
 
 config :pleroma, configurable_from_database: false
 
+config :pleroma, :mastodon_compatibility,
+  # https://git.pleroma.social/pleroma/pleroma/issues/1505
+  federated_note_replies_limit: 5
+
 config :swarm, node_blacklist: [~r/myhtml_.*$/]
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
index f941349d55d9497e620bf46399e004e19570ae1d..a0675ec308832aa7433df6bb14cf509fc9e964e7 100644 (file)
@@ -3089,6 +3089,20 @@ config :pleroma, :config_description, [
       }
     ]
   },
+  %{
+    group: :pleroma,
+    key: :mastodon_compatibility,
+    type: :group,
+    description: "Mastodon compatibility-related settings.",
+    children: [
+      %{
+        key: :federated_note_replies_limit,
+        type: :integer,
+        description:
+          "The number of Note self-reply URIs to be included with outgoing federation (`5` to mimic Mastodon hardcoded value, `0` to disable)."
+      }
+    ]
+  },
   %{
     group: :pleroma,
     type: :group,
index 896cbb3c5558d8bdd2b4431bc7cd4636f58ef9a3..b7be7a8007934318bd41d081636feaadc27ff4c6 100644 (file)
@@ -329,4 +329,23 @@ defmodule Pleroma.Activity do
       _ -> nil
     end
   end
+
+  def replies(activity, opts \\ []) do
+    object = Object.normalize(activity)
+
+    query =
+      Activity
+      |> Queries.by_type("Create")
+      |> Queries.by_object_in_reply_to_id(object.data["id"], skip_preloading: true)
+      |> order_by([activity], asc: activity.id)
+
+    if opts[:self_only] do
+      where(query, [a], a.actor == ^activity.actor)
+    else
+      query
+    end
+  end
+
+  def self_replies(activity, opts \\ []),
+    do: replies(activity, Keyword.put(opts, :self_only, true))
 end
index 79f3052016b5180c309ffc803f64c888214884ba..c17affec98501b973f346b259793b57d8ce73d9c 100644 (file)
@@ -7,7 +7,7 @@ defmodule Pleroma.Activity.Queries do
   Contains queries for Activity.
   """
 
-  import Ecto.Query, only: [from: 2]
+  import Ecto.Query, only: [from: 2, where: 3]
 
   @type query :: Ecto.Queryable.t() | Activity.t()
 
@@ -63,6 +63,22 @@ defmodule Pleroma.Activity.Queries do
     )
   end
 
+  @spec by_object_id(query, String.t()) :: query
+  def by_object_in_reply_to_id(query, in_reply_to_id, opts \\ []) do
+    query =
+      if opts[:skip_preloading] do
+        Activity.with_joined_object(query)
+      else
+        Activity.with_preloaded_object(query)
+      end
+
+    where(
+      query,
+      [activity, object: o],
+      fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(in_reply_to_id))
+    )
+  end
+
   @spec by_type(query, String.t()) :: query
   def by_type(query \\ Activity, activity_type) do
     from(
index 2b8bfc3bd2c00c179f2af1e3d216f56a8ff7a2be..9e712ab75b2257d45411bde100e1e43af622d3b0 100644 (file)
@@ -903,6 +903,49 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
 
   def set_reply_to_uri(obj), do: obj
 
+  @doc """
+  Serialized Mastodon-compatible `replies` collection containing _self-replies_.
+  Based on Mastodon's ActivityPub::NoteSerializer#replies.
+  """
+  def set_replies(obj) do
+    limit = Pleroma.Config.get([:mastodon_compatibility, :federated_note_replies_limit], 0)
+
+    replies_uris =
+      with true <- limit > 0 || nil,
+           %Activity{} = activity <- Activity.get_create_by_object_ap_id(obj["id"]) do
+        activity
+        |> Activity.self_replies()
+        |> select([a], fragment("?->>'id'", a.data))
+        |> limit(^limit)
+        |> Repo.all()
+      end
+
+    set_replies(obj, replies_uris || [])
+  end
+
+  defp set_replies(obj, replies_uris) when replies_uris in [nil, []] do
+    obj
+  end
+
+  defp set_replies(obj, replies_uris) do
+    # Note: stubs (Mastodon doesn't make separate requests via those URIs in FetchRepliesService)
+    masto_replies_uri = nil
+    masto_replies_next_page_uri = nil
+
+    replies_collection = %{
+      "type" => "Collection",
+      "id" => masto_replies_uri,
+      "first" => %{
+        "type" => "Collection",
+        "part_of" => masto_replies_uri,
+        "items" => replies_uris,
+        "next" => masto_replies_next_page_uri
+      }
+    }
+
+    Map.merge(obj, %{"replies" => replies_collection})
+  end
+
   # Prepares the object of an outgoing create activity.
   def prepare_object(object) do
     object
@@ -914,6 +957,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     |> prepare_attachments
     |> set_conversation
     |> set_reply_to_uri
+    |> set_replies
     |> strip_internal_fields
     |> strip_internal_tags
     |> set_type
index 5da358c43636e10fa0556dfbb3d4b72393c43bfe..418b8a1cae1092daafbae56536107b119a4083a3 100644 (file)
@@ -2027,4 +2027,51 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
              }
     end
   end
+
+  describe "set_replies/1" do
+    clear_config([:mastodon_compatibility, :federated_note_replies_limit]) do
+      Pleroma.Config.put([:mastodon_compatibility, :federated_note_replies_limit], 2)
+    end
+
+    test "returns unmodified object if activity doesn't have self-replies" do
+      data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
+      assert Transmogrifier.set_replies(data) == data
+    end
+
+    test "sets `replies` collection with a limited number of self-replies" do
+      [user, another_user] = insert_list(2, :user)
+
+      {:ok, %{id: id1} = activity} = CommonAPI.post(user, %{"status" => "1"})
+
+      {:ok, %{id: id2} = self_reply1} =
+        CommonAPI.post(user, %{"status" => "self-reply 1", "in_reply_to_status_id" => id1})
+
+      {:ok, self_reply2} =
+        CommonAPI.post(user, %{"status" => "self-reply 2", "in_reply_to_status_id" => id1})
+
+      # Assuming to _not_ be present in `replies` due to :federated_note_replies_limit is set to 2
+      {:ok, _} =
+        CommonAPI.post(user, %{"status" => "self-reply 3", "in_reply_to_status_id" => id1})
+
+      {:ok, _} =
+        CommonAPI.post(user, %{
+          "status" => "self-reply to self-reply",
+          "in_reply_to_status_id" => id2
+        })
+
+      {:ok, _} =
+        CommonAPI.post(another_user, %{
+          "status" => "another user's reply",
+          "in_reply_to_status_id" => id1
+        })
+
+      object = Object.normalize(activity)
+      replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.data["id"] end)
+
+      assert %{
+               "type" => "Collection",
+               "first" => %{"type" => "Collection", "items" => ^replies_uris}
+             } = Transmogrifier.set_replies(object.data)["replies"]
+    end
+  end
 end