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:
commit
aac5493dd5
9 changed files with 132 additions and 34 deletions
|
@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Unreleased
|
## 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
|
## 2025.03
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
|
@ -2022,12 +2022,13 @@ def get_or_fetch_by_ap_id(ap_id, options \\ []) do
|
||||||
Creates an internal service actor by URI if missing.
|
Creates an internal service actor by URI if missing.
|
||||||
Optionally takes nickname for addressing.
|
Optionally takes nickname for addressing.
|
||||||
"""
|
"""
|
||||||
@spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
|
@spec get_or_create_service_actor_by_ap_id(String.t(), String.t(), Keyword.t()) ::
|
||||||
def get_or_create_service_actor_by_ap_id(uri, nickname) do
|
User.t() | nil
|
||||||
|
def get_or_create_service_actor_by_ap_id(uri, nickname, create_opts \\ []) do
|
||||||
{_, user} =
|
{_, user} =
|
||||||
case get_cached_by_ap_id(uri) do
|
case get_cached_by_ap_id(uri) do
|
||||||
nil ->
|
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)}")
|
Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
|
||||||
{:error, nil}
|
{:error, nil}
|
||||||
end
|
end
|
||||||
|
@ -2049,15 +2050,27 @@ defp set_invisible(user) do
|
||||||
|> update_and_set_cache()
|
|> update_and_set_cache()
|
||||||
end
|
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()}
|
{:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||||
defp create_service_actor(uri, nickname) do
|
defp create_service_actor(uri, nickname, opts) do
|
||||||
%User{
|
%User{
|
||||||
invisible: true,
|
invisible: true,
|
||||||
local: true,
|
local: true,
|
||||||
ap_id: uri,
|
ap_id: uri,
|
||||||
nickname: nickname,
|
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
|
|> change
|
||||||
|> put_private_key()
|
|> put_private_key()
|
||||||
|
|
|
@ -16,7 +16,12 @@ defmodule Pleroma.Web.ActivityPub.Relay do
|
||||||
def ap_id, do: "#{Pleroma.Web.Endpoint.url()}/#{@nickname}"
|
def ap_id, do: "#{Pleroma.Web.Endpoint.url()}/#{@nickname}"
|
||||||
|
|
||||||
@spec get_actor() :: User.t() | nil
|
@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()}
|
@spec follow(String.t()) :: {:ok, Activity.t()} | {:error, any()}
|
||||||
def follow(target_instance) do
|
def follow(target_instance) do
|
||||||
|
|
|
@ -16,9 +16,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do
|
defp maybe_put(map, _, nil), do: map
|
||||||
%{"sharedInbox" => url(~p"/inbox")}
|
defp maybe_put(map, k, v), do: Map.put(map, k, v)
|
||||||
end
|
|
||||||
|
|
||||||
def render("endpoints.json", %{user: %User{local: true} = _user}) do
|
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,
|
"id" => user.ap_id,
|
||||||
"type" => "Application",
|
"type" => "Application",
|
||||||
"following" => "#{user.ap_id}/following",
|
|
||||||
"followers" => "#{user.ap_id}/followers",
|
|
||||||
"inbox" => "#{user.ap_id}/inbox",
|
"inbox" => "#{user.ap_id}/inbox",
|
||||||
"outbox" => "#{user.ap_id}/outbox",
|
"outbox" => "#{user.ap_id}/outbox",
|
||||||
"name" => "Pleroma",
|
"name" => "Akkoma",
|
||||||
"summary" =>
|
"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,
|
"url" => user.ap_id,
|
||||||
"manuallyApprovesFollowers" => false,
|
"manuallyApprovesFollowers" => false,
|
||||||
"publicKey" => %{
|
"publicKey" => %{
|
||||||
|
@ -56,16 +53,15 @@ def render("service.json", %{user: user}) do
|
||||||
"endpoints" => endpoints,
|
"endpoints" => endpoints,
|
||||||
"invisible" => User.invisible?(user)
|
"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())
|
|> Map.merge(Utils.make_json_ld_header())
|
||||||
end
|
end
|
||||||
|
|
||||||
# the instance itself is not a Person, but instead an Application
|
def render("user.json", %{user: %User{actor_type: "Application"} = user}),
|
||||||
def render("user.json", %{user: %User{nickname: nil} = user}),
|
|
||||||
do: render("service.json", %{user: 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
|
def render("user.json", %{user: user}) do
|
||||||
public_key =
|
public_key =
|
||||||
case User.SigningKey.public_key_pem(user) do
|
case User.SigningKey.public_key_pem(user) do
|
||||||
|
|
|
@ -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
|
|
@ -58,7 +58,11 @@ test "returns relay user" do
|
||||||
local: true,
|
local: true,
|
||||||
ap_id: ^uri,
|
ap_id: ^uri,
|
||||||
follower_address: ^followers_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 ->
|
assert capture_log(fn ->
|
||||||
refute User.get_or_create_service_actor_by_ap_id("/relay", "relay")
|
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
|
test "returns invisible actor" do
|
||||||
uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test"
|
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")
|
user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
|
||||||
|
|
||||||
assert %User{
|
assert %User{
|
||||||
|
@ -75,7 +78,7 @@ test "returns invisible actor" do
|
||||||
invisible: true,
|
invisible: true,
|
||||||
local: true,
|
local: true,
|
||||||
ap_id: ^uri,
|
ap_id: ^uri,
|
||||||
follower_address: ^followers_uri
|
follower_address: nil
|
||||||
} = user
|
} = user
|
||||||
|
|
||||||
user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
|
user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
|
||||||
|
|
32
test/pleroma/web/activity_pub/internal_fetch_actor_tests.exs
Normal file
32
test/pleroma/web/activity_pub/internal_fetch_actor_tests.exs
Normal 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
|
|
@ -20,6 +20,18 @@ test "gets an actor for the relay" do
|
||||||
assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay"
|
assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay"
|
||||||
end
|
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
|
test "relay actor is invisible" do
|
||||||
user = Relay.get_actor()
|
user = Relay.get_actor()
|
||||||
assert User.invisible?(user)
|
assert User.invisible?(user)
|
||||||
|
|
|
@ -126,18 +126,6 @@ test "remote users have an empty endpoints structure" do
|
||||||
assert result["id"] == user.ap_id
|
assert result["id"] == user.ap_id
|
||||||
assert result["endpoints"] == %{}
|
assert result["endpoints"] == %{}
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "followers" do
|
describe "followers" do
|
||||||
|
|
Loading…
Reference in a new issue