fix(activitypub): refetch a stale Move target once

A Move usually follows right after the alias was added, and the cached
remote target may not have it yet. Refetch the target when the alias is
missing instead of rejecting the Move. Same issue as Akkoma f18e2ba42.
This commit is contained in:
Lain Soykaf 2026-10-31 15:53:41 +04:00
commit 3cd201c365
No known key found for this signature in database
3 changed files with 65 additions and 1 deletions

View file

@ -0,0 +1 @@
Accept a Move when the cached remote target does not list the alias yet, by refetching the target once

View file

@ -799,7 +799,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
) do
with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
true <- origin_actor in target_user.also_known_as do
{:ok, %User{} = target_user} <- refetch_unless_aliased(target_user, origin_actor) do
ActivityPub.move(origin_user, target_user, false)
else
_e -> :error
@ -1204,4 +1204,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def maybe_fix_user_url(data), do: data
def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
# A Move usually follows right after the alias was added, which the cached
# remote target may not have yet.
defp refetch_unless_aliased(%User{} = target, origin_ap_id, refetch? \\ true) do
cond do
origin_ap_id in target.also_known_as ->
{:ok, target}
refetch? and not target.local ->
with {:ok, %User{} = target} <- User.fetch_by_ap_id(target.ap_id),
do: refetch_unless_aliased(target, origin_ap_id, false)
true ->
:error
end
end
end

View file

@ -147,6 +147,53 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert activity.data["type"] == "Move"
end
test "it refetches a cached remote Move target that lacks the alias" do
old_user = insert(:user, local: false, ap_id: "https://old.example/users/old")
new_user =
insert(:user,
local: false,
ap_id: "https://new.example/users/new",
nickname: "new@new.example",
last_refreshed_at: NaiveDateTime.utc_now()
)
actor = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => new_user.ap_id,
"type" => "Person",
"preferredUsername" => "new",
"inbox" => new_user.ap_id <> "/inbox",
"outbox" => new_user.ap_id <> "/outbox",
"followers" => new_user.ap_id <> "/followers",
"following" => new_user.ap_id <> "/following",
"alsoKnownAs" => [old_user.ap_id]
}
Tesla.Mock.mock(fn
%{method: :get, url: "https://new.example/users/new"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body: Jason.encode!(actor)
}
_ ->
%Tesla.Env{status: 404}
end)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Move",
"actor" => old_user.ap_id,
"object" => old_user.ap_id,
"target" => new_user.ap_id
}
assert {:ok, %Activity{data: %{"type" => "Move"}}} = Transmogrifier.handle_incoming(message)
assert old_user.ap_id in User.get_cached_by_ap_id(new_user.ap_id).also_known_as
end
test "it fixes both the Create and object contexts in a reply" do
insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
insert(:user, ap_id: "https://p.helene.moe/users/helene")