web/metadata: provide alternate link for ActivityPub
This allows discovering a page represents an ActivityPub object and also where to find the underlying representation. Other servers already implement this and some tools came to rely or profit from it. The alternate link is provided both with the "application/activity+json" format as used by Mastodon and the standard-compliant media type. Just like the feed provider, ActivityPub links are always enabled unless access to local posts is restricted and not configurable. The commit is based on earlier work by Charlotte 🦝 Deleńkec but with fixes and some tweaks. Co-authored-by: Charlotte 🦝 Deleńkec <lotte@chir.rs>
This commit is contained in:
parent
6a6d4254d5
commit
ed03eaade9
4 changed files with 91 additions and 1 deletions
|
@ -64,7 +64,11 @@ def activity_nsfw?(_) do
|
||||||
|
|
||||||
defp activated_providers do
|
defp activated_providers do
|
||||||
unless Pleroma.Config.restrict_unauthenticated_access?(:activities, :local) 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
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
35
lib/pleroma/web/metadata/providers/ap_url.ex
Normal file
35
lib/pleroma/web/metadata/providers/ap_url.ex
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Akkoma: Magically expressive social media
|
||||||
|
# Copyright © 2025 Akkoma Authors <https://akkoma.dev/>
|
||||||
|
# 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
|
31
test/pleroma/web/metadata/providers/ap_url_test.exs
Normal file
31
test/pleroma/web/metadata/providers/ap_url_test.exs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# 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
|
|
@ -321,6 +321,16 @@ defp meta_find_twitter(document, name) do
|
||||||
Floki.find(document, "head>meta[name=\"twitter:" <> name <> "\"]")
|
Floki.find(document, "head>meta[name=\"twitter:" <> name <> "\"]")
|
||||||
end
|
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
|
# 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
|
# one check per type of content should suffice to ensure we're calling the providers correctly
|
||||||
describe "metadata tags for" do
|
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_desc, _}] = meta_find_twitter(document, "description")
|
||||||
[{"meta", tw_img, _}] = meta_find_twitter(document, "image")
|
[{"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_type) == "article"
|
||||||
assert meta_content(og_title) == Pleroma.Web.Metadata.Utils.user_name_string(user)
|
assert meta_content(og_title) == Pleroma.Web.Metadata.Utils.user_name_string(user)
|
||||||
assert meta_content(og_url) == user.ap_id
|
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_title) == meta_content(og_title)
|
||||||
assert meta_content(tw_desc) == meta_content(og_desc)
|
assert meta_content(tw_desc) == meta_content(og_desc)
|
||||||
assert meta_content(tw_img) == meta_content(og_img)
|
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
|
end
|
||||||
|
|
||||||
test "text-only post", %{conn: conn, user: user, user_avatar_url: user_avatar_url} do
|
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_desc, _}] = meta_find_twitter(document, "description")
|
||||||
[{"meta", tw_img, _}] = meta_find_twitter(document, "image")
|
[{"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_type) == "article"
|
||||||
assert meta_content(og_title) == Pleroma.Web.Metadata.Utils.user_name_string(user)
|
assert meta_content(og_title) == Pleroma.Web.Metadata.Utils.user_name_string(user)
|
||||||
assert meta_content(og_url) == activity.data["id"]
|
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_title) == meta_content(og_title)
|
||||||
assert meta_content(tw_desc) == meta_content(og_desc)
|
assert meta_content(tw_desc) == meta_content(og_desc)
|
||||||
assert meta_content(tw_img) == meta_content(og_img)
|
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
|
end
|
||||||
|
|
||||||
test "post with attachments", %{conn: conn, user: user} do
|
test "post with attachments", %{conn: conn, user: user} do
|
||||||
|
|
Loading…
Reference in a new issue