diff --git a/changelog.d/featured-collection-page.fix b/changelog.d/featured-collection-page.fix new file mode 100644 index 000000000..b611a4137 --- /dev/null +++ b/changelog.d/featured-collection-page.fix @@ -0,0 +1 @@ +Support fetching featured collection items from the first collection page diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 0b513ee16..1e8bfe3dd 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1881,11 +1881,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end + @featured_collection_types ["OrderedCollection", "Collection"] + @featured_collection_page_types ["OrderedCollectionPage", "CollectionPage"] + @featured_collection_item_types @featured_collection_types ++ @featured_collection_page_types + def pin_data_from_featured_collection(%{ "type" => type, "orderedItems" => objects }) - when type in ["OrderedCollection", "Collection"] do + when type in @featured_collection_item_types do Map.new(objects, fn %{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()} object_ap_id when is_binary(object_ap_id) -> {object_ap_id, NaiveDateTime.utc_now()} @@ -1903,7 +1907,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def fetch_and_prepare_featured_from_ap_id(ap_id) do with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do - {:ok, pin_data_from_featured_collection(data)} + {:ok, prepare_featured_collection(data)} else e -> Logger.error("Could not decode featured collection at fetch #{ap_id}, #{inspect(e)}") @@ -1911,6 +1915,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end + defp prepare_featured_collection(%{"orderedItems" => objects} = data) when is_list(objects) do + pin_data_from_featured_collection(data) + end + + defp prepare_featured_collection(%{ + "type" => type, + "first" => %{"type" => page_type} = first + }) + when type in @featured_collection_types and page_type in @featured_collection_page_types do + pin_data_from_featured_collection(first) + end + + defp prepare_featured_collection(%{"type" => type, "first" => first}) + when type in @featured_collection_types and is_binary(first) do + case Fetcher.fetch_and_contain_remote_object_from_id(first) do + {:ok, data} -> + pin_data_from_featured_collection(data) + + e -> + Logger.error("Could not decode featured collection page at fetch #{first}, #{inspect(e)}") + %{} + end + end + + defp prepare_featured_collection(data) do + pin_data_from_featured_collection(data) + end + def enqueue_pin_fetches(%{pinned_objects: pins}) do # enqueue a task to fetch all pinned objects Enum.each(pins, fn {ap_id, _} -> diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 9aafc41a5..dd7a82a55 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -415,6 +415,105 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url) end + test "fetches user featured collection from the first collection page" do + ap_id = "https://example.com/users/lain" + featured_url = "https://example.com/users/lain/collections/featured" + first_page_url = "#{featured_url}?page=true" + + user_data = + "test/fixtures/users_mock/user.json" + |> File.read!() + |> String.replace("{{nickname}}", "lain") + |> Jason.decode!() + |> Map.put("featured", featured_url) + |> Jason.encode!() + + object_id = Ecto.UUID.generate() + object_url = "https://example.com/objects/#{object_id}" + + featured_data = + Jason.encode!(%{ + "id" => featured_url, + "type" => "OrderedCollection", + "first" => first_page_url + }) + + first_page_data = + Jason.encode!(%{ + "id" => first_page_url, + "type" => "OrderedCollectionPage", + "partOf" => featured_url, + "orderedItems" => [object_url] + }) + + object_data = + "test/fixtures/statuses/note.json" + |> File.read!() + |> String.replace("{{object_id}}", object_id) + |> String.replace("{{nickname}}", "lain") + + Tesla.Mock.mock(fn + %{ + method: :get, + url: ^ap_id + } -> + %Tesla.Env{ + status: 200, + body: user_data, + headers: [{"content-type", "application/activity+json"}] + } + + %{ + method: :get, + url: ^featured_url + } -> + %Tesla.Env{ + status: 200, + body: featured_data, + headers: [{"content-type", "application/activity+json"}] + } + + %{ + method: :get, + url: ^first_page_url + } -> + %Tesla.Env{ + status: 200, + body: first_page_data, + headers: [{"content-type", "application/activity+json"}] + } + + %{ + method: :get, + url: ^object_url + } -> + %Tesla.Env{ + status: 200, + body: object_data, + headers: [{"content-type", "application/activity+json"}] + } + end) + + refute capture_log(fn -> + {:ok, user} = ActivityPub.make_user_from_ap_id(ap_id) + + assert_enqueued( + worker: Pleroma.Workers.RemoteFetcherWorker, + args: %{ + "op" => "fetch_remote", + "id" => object_url, + "depth" => 1 + } + ) + + Pleroma.Tests.ObanHelpers.perform_all() + + assert user.featured_address == featured_url + assert Map.has_key?(user.pinned_objects, object_url) + assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url) + end) =~ "Could not parse featured collection" + end + test "fetches user birthday information from misskey" do user_id = "https://misskey.io/@mkljczk" @@ -2818,4 +2917,38 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do "first" => "https://social.example/users/alice/collections/featured?page=true" }) end + + test "fetch_and_prepare_featured_from_ap_id handles embedded first collection pages" do + featured_url = "https://social.example/users/alice/collections/featured" + first_page_url = "#{featured_url}?page=true" + object_url = "https://social.example/objects/1" + + featured_data = + Jason.encode!(%{ + "id" => featured_url, + "type" => "OrderedCollection", + "first" => %{ + "id" => first_page_url, + "type" => "OrderedCollectionPage", + "partOf" => featured_url, + "orderedItems" => [object_url] + } + }) + + Tesla.Mock.mock(fn + %{method: :get, url: ^featured_url} -> + %Tesla.Env{ + status: 200, + body: featured_data, + headers: [{"content-type", "application/activity+json"}] + } + end) + + refute capture_log(fn -> + assert {:ok, pinned_objects} = + ActivityPub.fetch_and_prepare_featured_from_ap_id(featured_url) + + assert Map.has_key?(pinned_objects, object_url) + end) =~ "Could not parse featured collection" + end end