diff --git a/lib/pleroma/web/metadata.ex b/lib/pleroma/web/metadata.ex index 48801b588..f232fbb0e 100644 --- a/lib/pleroma/web/metadata.ex +++ b/lib/pleroma/web/metadata.ex @@ -64,7 +64,11 @@ def activity_nsfw?(_) do defp activated_providers do unless Pleroma.Config.restrict_unauthenticated_access?(:activities, :local) do - [Pleroma.Web.Metadata.Providers.Feed | Pleroma.Config.get([__MODULE__, :providers], [])] + [ + Pleroma.Web.Metadata.Providers.Feed, + Pleroma.Web.Metadata.Providers.ApUrl + | Pleroma.Config.get([__MODULE__, :providers], []) + ] else [] end diff --git a/lib/pleroma/web/metadata/providers/ap_url.ex b/lib/pleroma/web/metadata/providers/ap_url.ex new file mode 100644 index 000000000..cff581713 --- /dev/null +++ b/lib/pleroma/web/metadata/providers/ap_url.ex @@ -0,0 +1,35 @@ +# Akkoma: Magically expressive social media +# Copyright © 2025 Akkoma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.ApUrl do + alias Pleroma.Web.Metadata.Providers.Provider + + @behaviour Provider + + defp alt_link(uri, type) do + { + :link, + [rel: "alternate", href: uri, type: type], + [] + } + end + + defp ap_alt_links(uri) do + [ + alt_link(uri, "application/activity+json"), + alt_link(uri, "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + ] + end + + @impl Provider + def build_tags(%{object: %{data: %{"id" => ap_id}}}) when is_binary(ap_id) do + ap_alt_links(ap_id) + end + + def build_tags(%{user: %{ap_id: ap_id}}) when is_binary(ap_id) do + ap_alt_links(ap_id) + end + + def build_tags(_), do: [] +end diff --git a/test/pleroma/web/metadata/providers/ap_url_test.exs b/test/pleroma/web/metadata/providers/ap_url_test.exs new file mode 100644 index 000000000..b01c58fff --- /dev/null +++ b/test/pleroma/web/metadata/providers/ap_url_test.exs @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.ApUrlTest do + use Pleroma.DataCase, async: true + import Pleroma.Factory + alias Pleroma.Web.Metadata.Providers.ApUrl + + @ap_type_compliant "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + @ap_type_mastodon "application/activity+json" + + test "it preferentially renders a link to a post" do + user = insert(:user) + note = insert(:note, user: user) + + assert ApUrl.build_tags(%{object: note, user: user}) == [ + {:link, [rel: "alternate", href: note.data["id"], type: @ap_type_mastodon], []}, + {:link, [rel: "alternate", href: note.data["id"], type: @ap_type_compliant], []} + ] + end + + test "it renders a link to a user" do + user = insert(:user) + + assert ApUrl.build_tags(%{user: user}) == [ + {:link, [rel: "alternate", href: user.ap_id, type: @ap_type_mastodon], []}, + {:link, [rel: "alternate", href: user.ap_id, type: @ap_type_compliant], []} + ] + end +end diff --git a/test/pleroma/web/static_fe/static_fe_controller_test.exs b/test/pleroma/web/static_fe/static_fe_controller_test.exs index 79d4d6261..4d128af73 100644 --- a/test/pleroma/web/static_fe/static_fe_controller_test.exs +++ b/test/pleroma/web/static_fe/static_fe_controller_test.exs @@ -321,6 +321,16 @@ defp meta_find_twitter(document, name) do Floki.find(document, "head>meta[name=\"twitter:" <> name <> "\"]") end + defp meta_find_alt_links(document) do + Floki.find(document, "head>link[rel=\"alternate\"]") + |> Enum.map(fn {_, attr, _} -> + { + :proplists.get_value("type", attr), + :proplists.get_value("href", attr) + } + end) + end + # Detailed metadata tests are already done for each builder individually, so just # one check per type of content should suffice to ensure we're calling the providers correctly describe "metadata tags for" do @@ -350,6 +360,8 @@ test "user profile", %{conn: conn, user: user, user_avatar_url: user_avatar_url} [{"meta", tw_desc, _}] = meta_find_twitter(document, "description") [{"meta", tw_img, _}] = meta_find_twitter(document, "image") + alt_links = meta_find_alt_links(document) + assert meta_content(og_type) == "article" assert meta_content(og_title) == Pleroma.Web.Metadata.Utils.user_name_string(user) assert meta_content(og_url) == user.ap_id @@ -362,6 +374,8 @@ test "user profile", %{conn: conn, user: user, user_avatar_url: user_avatar_url} assert meta_content(tw_title) == meta_content(og_title) assert meta_content(tw_desc) == meta_content(og_desc) assert meta_content(tw_img) == meta_content(og_img) + + assert Enum.any?(alt_links, fn e -> e == {"application/activity+json", user.ap_id} end) end test "text-only post", %{conn: conn, user: user, user_avatar_url: user_avatar_url} do @@ -386,6 +400,8 @@ test "text-only post", %{conn: conn, user: user, user_avatar_url: user_avatar_ur [{"meta", tw_desc, _}] = meta_find_twitter(document, "description") [{"meta", tw_img, _}] = meta_find_twitter(document, "image") + alt_links = meta_find_alt_links(document) + assert meta_content(og_type) == "article" assert meta_content(og_title) == Pleroma.Web.Metadata.Utils.user_name_string(user) assert meta_content(og_url) == activity.data["id"] @@ -398,6 +414,10 @@ test "text-only post", %{conn: conn, user: user, user_avatar_url: user_avatar_ur assert meta_content(tw_title) == meta_content(og_title) assert meta_content(tw_desc) == meta_content(og_desc) assert meta_content(tw_img) == meta_content(og_img) + + assert Enum.any?(alt_links, fn e -> + e == {"application/activity+json", activity.object.data["id"]} + end) end test "post with attachments", %{conn: conn, user: user} do