Fix representaton of internal actors
Some checks failed
ci/woodpecker/pr/build-amd64 Pipeline is pending
ci/woodpecker/pr/docs Pipeline is pending
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test/2 Pipeline was successful
ci/woodpecker/pr/test/1 Pipeline was successful
ci/woodpecker/pr/build-arm64 Pipeline was successful
ci/woodpecker/pull_request_closed/lint Pipeline is pending
ci/woodpecker/pull_request_closed/test/1 unknown status
ci/woodpecker/pull_request_closed/test/2 unknown status
ci/woodpecker/pull_request_closed/build-arm64 unknown status
ci/woodpecker/pull_request_closed/build-amd64 unknown status
ci/woodpecker/pull_request_closed/docs unknown status

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 70410dfafd.
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 #457 but accidentally
reverted during a translation update in
eba3cce77b. 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: #855

Co-authored-by: ilja space <git@ilja.space>
This commit is contained in:
Oneric 2024-11-22 22:24:21 +01:00
parent b58b6af3ba
commit caf6b4606f
9 changed files with 130 additions and 32 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,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

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