Merge pull request 'Don’t pretend internal actors have follow(er|ing) collections' (#856) from Oneric/akkoma:fetch-actor-follow-collections into develop

Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/856
This commit is contained in:
Oneric 2025-05-09 20:10:41 +00:00
commit aac5493dd5
9 changed files with 132 additions and 34 deletions

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,41 @@
defmodule Pleroma.Repo.Migrations.InstanceActorsTweaks do
use Ecto.Migration
import Ecto.Query
def up() do
# since Akkoma isnt up and running at this point, Web.endpoint()
# isnt 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

View file

@ -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")

View file

@ -0,0 +1,32 @@
# Akkoma: Magically expressive social media
# Copyright © 2025 Akkoma Authors <https://akkoma.dev/>
# 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

View file

@ -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 <https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application>
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)

View file

@ -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