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.
}
]
},
+ %{
+ 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,
_ -> 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
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()
)
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(
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
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
+ |> set_replies
|> strip_internal_fields
|> strip_internal_tags
|> set_type
}
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