diff --git a/CHANGELOG.md b/CHANGELOG.md index f819fb0e8..4c18d2690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Added + +### Fixed +- Internal actors no longer pretend to have unresolvable follow(er|ing) collections + +### Changed +- Internal and relay actors are now again represented with type "Application" + ## 2025.03 ## Added diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 5f3ddf64a..f7a9c82f8 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -2022,12 +2022,13 @@ def get_or_fetch_by_ap_id(ap_id, options \\ []) do Creates an internal service actor by URI if missing. Optionally takes nickname for addressing. """ - @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil - def get_or_create_service_actor_by_ap_id(uri, nickname) do + @spec get_or_create_service_actor_by_ap_id(String.t(), String.t(), Keyword.t()) :: + User.t() | nil + def get_or_create_service_actor_by_ap_id(uri, nickname, create_opts \\ []) do {_, user} = case get_cached_by_ap_id(uri) do nil -> - with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do + with {:error, %{errors: errors}} <- create_service_actor(uri, nickname, create_opts) do Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}") {:error, nil} end @@ -2049,15 +2050,27 @@ defp set_invisible(user) do |> update_and_set_cache() end - @spec create_service_actor(String.t(), String.t()) :: + @spec create_service_actor(String.t(), String.t(), Keyword.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} - defp create_service_actor(uri, nickname) do + defp create_service_actor(uri, nickname, opts) do %User{ invisible: true, local: true, ap_id: uri, nickname: nickname, - follower_address: uri <> "/followers" + actor_type: "Application", + follower_address: + if Keyword.get(opts, :followable, false) do + uri <> "/followers" + else + nil + end, + following_address: + if Keyword.get(opts, :following, false) do + uri <> "/following" + else + nil + end } |> change |> put_private_key() diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index 6d60a074f..4d6413736 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -16,7 +16,12 @@ defmodule Pleroma.Web.ActivityPub.Relay do def ap_id, do: "#{Pleroma.Web.Endpoint.url()}/#{@nickname}" @spec get_actor() :: User.t() | nil - def get_actor, do: User.get_or_create_service_actor_by_ap_id(ap_id(), @nickname) + def get_actor, + do: + User.get_or_create_service_actor_by_ap_id(ap_id(), @nickname, + followable: true, + following: true + ) @spec follow(String.t()) :: {:ok, Activity.t()} | {:error, any()} def follow(target_instance) do diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 2ca31fc3c..2a8c6df21 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -16,9 +16,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do import Ecto.Query - def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do - %{"sharedInbox" => url(~p"/inbox")} - end + defp maybe_put(map, _, nil), do: map + defp maybe_put(map, k, v), do: Map.put(map, k, v) def render("endpoints.json", %{user: %User{local: true} = _user}) do %{ @@ -39,13 +38,11 @@ def render("service.json", %{user: user}) do %{ "id" => user.ap_id, "type" => "Application", - "following" => "#{user.ap_id}/following", - "followers" => "#{user.ap_id}/followers", "inbox" => "#{user.ap_id}/inbox", "outbox" => "#{user.ap_id}/outbox", - "name" => "Pleroma", + "name" => "Akkoma", "summary" => - "An internal service actor for this Pleroma instance. No user-serviceable parts inside.", + "An internal service actor for this Akkoma instance. No user-serviceable parts inside.", "url" => user.ap_id, "manuallyApprovesFollowers" => false, "publicKey" => %{ @@ -56,16 +53,15 @@ def render("service.json", %{user: user}) do "endpoints" => endpoints, "invisible" => User.invisible?(user) } + |> maybe_put("following", user.following_address) + |> maybe_put("followers", user.follower_address) + |> maybe_put("preferredUsername", user.nickname) |> Map.merge(Utils.make_json_ld_header()) end - # the instance itself is not a Person, but instead an Application - def render("user.json", %{user: %User{nickname: nil} = user}), + def render("user.json", %{user: %User{actor_type: "Application"} = user}), do: render("service.json", %{user: user}) - def render("user.json", %{user: %User{nickname: "internal." <> _} = user}), - do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname) - def render("user.json", %{user: user}) do public_key = case User.SigningKey.public_key_pem(user) do diff --git a/priv/repo/migrations/20250326000000_instance_actor_tweaks.exs b/priv/repo/migrations/20250326000000_instance_actor_tweaks.exs new file mode 100644 index 000000000..86fe9f41d --- /dev/null +++ b/priv/repo/migrations/20250326000000_instance_actor_tweaks.exs @@ -0,0 +1,41 @@ +defmodule Pleroma.Repo.Migrations.InstanceActorsTweaks do + use Ecto.Migration + + import Ecto.Query + + def up() do + # since Akkoma isn’t up and running at this point, Web.endpoint() + # isn’t available and we can't use the functions from Relay and InternalFetchActor, + # thus the AP ID suffix are hardcoded here and used together with a check for locality + # (e.g. custom ports make it hard to hardcode the full base url) + relay_ap_id = "%/relay" + fetch_ap_id = "%/internal/fetch" + + # Convert to Application type + Pleroma.User + |> where([u], u.local and (like(u.ap_id, ^fetch_ap_id) or like(u.ap_id, ^relay_ap_id))) + |> Pleroma.Repo.update_all(set: [actor_type: "Application"]) + + # Drop bogus follow* addresses + Pleroma.User + |> where([u], u.local and like(u.ap_id, ^fetch_ap_id)) + |> Pleroma.Repo.update_all(set: [follower_address: nil, following_address: nil]) + + # Add required follow* addresses + Pleroma.User + |> where([u], u.local and like(u.ap_id, ^relay_ap_id)) + |> update([u], + set: [ + follower_address: fragment("CONCAT(?, '/followers')", u.ap_id), + following_address: fragment("CONCAT(?, '/following')", u.ap_id) + ] + ) + |> Pleroma.Repo.update_all([]) + end + + def down do + # We don't know if the type was Person or Application before and + # without this or the lost patch it didn't matter, so just do nothing + :ok + end +end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index be8e21dcb..890109a4a 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -58,7 +58,11 @@ test "returns relay user" do local: true, ap_id: ^uri, follower_address: ^followers_uri - } = User.get_or_create_service_actor_by_ap_id(uri, "relay") + } = + User.get_or_create_service_actor_by_ap_id(uri, "relay", + followable: true, + following: true + ) assert capture_log(fn -> refute User.get_or_create_service_actor_by_ap_id("/relay", "relay") @@ -67,7 +71,6 @@ test "returns relay user" do test "returns invisible actor" do uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test" - followers_uri = "#{uri}/followers" user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test") assert %User{ @@ -75,7 +78,7 @@ test "returns invisible actor" do invisible: true, local: true, ap_id: ^uri, - follower_address: ^followers_uri + follower_address: nil } = user user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test") diff --git a/test/pleroma/web/activity_pub/internal_fetch_actor_tests.exs b/test/pleroma/web/activity_pub/internal_fetch_actor_tests.exs new file mode 100644 index 000000000..ae1828f79 --- /dev/null +++ b/test/pleroma/web/activity_pub/internal_fetch_actor_tests.exs @@ -0,0 +1,32 @@ +# Akkoma: Magically expressive social media +# Copyright © 2025 Akkoma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.InternalFetchActorTests do + use Pleroma.DataCase, async: true + + alias Pleroma.User + alias Pleroma.Web.ActivityPub.InternalFetchActor + + test "creates a fetch actor if needed" do + user = InternalFetchActor.get_actor() + assert user + assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/internal/fetch" + end + + test "fetch actor is an application" do + user = InternalFetchActor.get_actor() + assert user.actor_type == "Application" + end + + test "fetch actor doesn't expose follow* collections" do + user = InternalFetchActor.get_actor() + refute user.follower_address + refute user.following_address + end + + test "fetch actor is invisible" do + user = InternalFetchActor.get_actor() + assert User.invisible?(user) + end +end diff --git a/test/pleroma/web/activity_pub/relay_test.exs b/test/pleroma/web/activity_pub/relay_test.exs index b1c927b49..18407ac09 100644 --- a/test/pleroma/web/activity_pub/relay_test.exs +++ b/test/pleroma/web/activity_pub/relay_test.exs @@ -20,6 +20,18 @@ test "gets an actor for the relay" do assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay" end + test "relay actor is an application" do + # See + user = Relay.get_actor() + assert user.actor_type == "Application" + end + + test "relay actor has follow* collections" do + user = Relay.get_actor() + assert user.follower_address + assert user.following_address + end + test "relay actor is invisible" do user = Relay.get_actor() assert User.invisible?(user) diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs index 4283fb0c8..e2c8f5865 100644 --- a/test/pleroma/web/activity_pub/views/user_view_test.exs +++ b/test/pleroma/web/activity_pub/views/user_view_test.exs @@ -126,18 +126,6 @@ test "remote users have an empty endpoints structure" do assert result["id"] == user.ap_id assert result["endpoints"] == %{} end - - test "instance users do not expose oAuth endpoints" do - user = - insert(:user, nickname: nil, local: true) - |> with_signing_key() - - result = UserView.render("user.json", %{user: user}) - - refute result["endpoints"]["oauthAuthorizationEndpoint"] - refute result["endpoints"]["oauthRegistrationEndpoint"] - refute result["endpoints"]["oauthTokenEndpoint"] - end end describe "followers" do