From b58b6af3ba0a0a8badbf0f1c3fba753112f3bf54 Mon Sep 17 00:00:00 2001 From: Oneric Date: Fri, 22 Nov 2024 22:34:41 +0100 Subject: [PATCH 1/2] cosmetic: adapt software name in internal actor descriptions --- lib/pleroma/web/activity_pub/views/user_view.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 2ca31fc3c..34d3c65cc 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -43,9 +43,9 @@ def render("service.json", %{user: user}) do "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" => %{ From caf6b4606fb9ce44092f565be3d51d7719915f52 Mon Sep 17 00:00:00 2001 From: Oneric Date: Fri, 22 Nov 2024 22:24:21 +0100 Subject: [PATCH 2/2] Fix representaton of internal actors CUrrently internal actors are supposed to be identified in the database by either a NULL nickname or a nickname prefixed by "internal.". For old installations this is true, but only if they were created over five years ago before 70410dfafd272bd1f38602446cc4f6e83645326f. Newer installations will use "relay" as the nickname of the realy actor causing ii to be treated as a regular user. In particular this means all installations in the last five years never made use of the reduced endpoint case, thus it is dropped. Simplify this distinction by properly marking internal actors asa an Application type in the database. This was already implemented before by ilja in https://akkoma.dev/AkkomaGang/akkoma/pulls/457 but accidentally reverted during a translation update in eba3cce77b45e89028f7e6bfa708b469b7416914. This commit effectively restores this patch together with further changes. Also service actors unconditionally expose follow* collections atm, eventhough the internal fetch actor doesn't actually implement them. Since they are optional per spec and with Mastodon omitting them too for its instance actor proving the practical viability, we should just omit them. The relay actor however should continue to expose such collections and they are properly implemented here. Here too we now just use the values or their absence in the database. We do not have any other internal.* actors besides fetch atm. Fixes: https://akkoma.dev/AkkomaGang/akkoma/issues/855 Co-authored-by: ilja space --- CHANGELOG.md | 8 ++++ lib/pleroma/user.ex | 25 ++++++++--- lib/pleroma/web/activity_pub/relay.ex | 7 +++- .../web/activity_pub/views/user_view.ex | 16 +++----- .../20250326000000_instance_actor_tweaks.exs | 41 +++++++++++++++++++ test/pleroma/user_test.exs | 9 ++-- .../internal_fetch_actor_tests.exs | 32 +++++++++++++++ test/pleroma/web/activity_pub/relay_test.exs | 12 ++++++ .../web/activity_pub/views/user_view_test.exs | 12 ------ 9 files changed, 130 insertions(+), 32 deletions(-) create mode 100644 priv/repo/migrations/20250326000000_instance_actor_tweaks.exs create mode 100644 test/pleroma/web/activity_pub/internal_fetch_actor_tests.exs 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 34d3c65cc..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,8 +38,6 @@ 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" => "Akkoma", @@ -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