AP ingest: normalize inReplyTo shapes

This commit is contained in:
Lain Soykaf 2026-01-16 23:57:01 +04:00
commit bff67650e1
8 changed files with 245 additions and 12 deletions

View file

@ -41,22 +41,34 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
def fix_object_defaults(data) do
data = Maps.filter_empty_values(data)
in_reply_to_id = in_reply_to_id(data["inReplyTo"])
context =
Utils.maybe_create_context(
data["context"] || data["conversation"] || data["inReplyTo"] || data["id"]
data["context"] || data["conversation"] || in_reply_to_id || data["id"]
)
%User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"])
data = Map.put(data, "context", context)
data
|> Map.put("context", context)
|> cast_and_filter_recipients("to", follower_collection)
|> cast_and_filter_recipients("cc", follower_collection)
|> cast_and_filter_recipients("bto", follower_collection)
|> cast_and_filter_recipients("bcc", follower_collection)
|> Transmogrifier.fix_implicit_addressing(follower_collection)
with attributed_to when is_binary(attributed_to) <- data["attributedTo"],
%User{follower_address: follower_collection} <- User.get_cached_by_ap_id(attributed_to) do
data
|> cast_and_filter_recipients("to", follower_collection)
|> cast_and_filter_recipients("cc", follower_collection)
|> cast_and_filter_recipients("bto", follower_collection)
|> cast_and_filter_recipients("bcc", follower_collection)
|> Transmogrifier.fix_implicit_addressing(follower_collection)
else
_ -> data
end
end
defp in_reply_to_id(in_reply_to) when is_binary(in_reply_to), do: in_reply_to
defp in_reply_to_id(%{"id" => in_reply_to}) when is_binary(in_reply_to), do: in_reply_to
defp in_reply_to_id(%{"href" => in_reply_to}) when is_binary(in_reply_to), do: in_reply_to
defp in_reply_to_id([in_reply_to | _]), do: in_reply_to_id(in_reply_to)
defp in_reply_to_id(_), do: nil
def fix_activity_addressing(activity) do
%User{follower_address: follower_collection} = User.get_cached_by_ap_id(activity["actor"])

View file

@ -201,9 +201,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do
in_reply_to_id = prepare_in_reply_to(in_reply_to)
object =
cond do
is_list(in_reply_to) ->
if not_empty_string(in_reply_to_id) do
Map.put(object, "inReplyTo", in_reply_to_id)
else
Map.delete(object, "inReplyTo")
end
is_map(in_reply_to) and is_nil(in_reply_to["id"]) and is_binary(in_reply_to["href"]) ->
Map.put(object, "inReplyTo", in_reply_to["href"])
is_map(in_reply_to) and is_nil(in_reply_to["id"]) and is_nil(in_reply_to["href"]) ->
Map.delete(object, "inReplyTo")
true ->
object
end
depth = (options[:depth] || 0) + 1
if Federator.allowed_thread_distance?(depth) do
if not_empty_string(in_reply_to_id) and Federator.allowed_thread_distance?(depth) do
with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
%Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
@ -249,9 +269,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
in_reply_to["id"]
is_map(in_reply_to) && is_bitstring(in_reply_to["href"]) ->
in_reply_to["href"]
is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
Enum.at(in_reply_to, 0)
is_list(in_reply_to) && is_map(Enum.at(in_reply_to, 0)) ->
in_reply_to
|> Enum.at(0)
|> prepare_in_reply_to()
true ->
""
end

View file

@ -0,0 +1,30 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/in-reply-to-collection",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:08Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-in-reply-to-collection",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:08Z",
"content": "collection",
"inReplyTo": {
"type": "Collection",
"items": [
"https://fedidev.fun/users/alice"
]
}
}
}

View file

@ -0,0 +1,35 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/in-reply-to-embedded-note",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:21Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-in-reply-to-embedded-note",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:21Z",
"content": "embedded note",
"inReplyTo": {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"id": "https://fedidev.fun/assets/note1.jsonap",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"attributedTo": "https://fedidev.fun/users/alice",
"content": "One",
"published": "2024-01-06T13:11:45Z"
}
}
}

View file

@ -0,0 +1,28 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/in-reply-to-link",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:39:56Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-in-reply-to-link",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:39:56Z",
"content": "Link in inReplyTo",
"inReplyTo": {
"type": "Link",
"href": "https://fedidev.fun/assets/note1.jsonap"
}
}
}

View file

@ -0,0 +1,28 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/in-reply-to-list",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:08Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-in-reply-to-list",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:08Z",
"content": "two elements in inReplyTo",
"inReplyTo": [
"https://fedidev.fun/assets/note1.jsonap",
"https://fedidev.fun/assets/note2.jsonap"
]
}
}

View file

@ -0,0 +1,72 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunInReplyToTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
case User.get_cached_by_ap_id(ap_id) do
%User{} ->
:ok
_ ->
insert(:user,
local: false,
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following",
featured_address: ap_id <> "/collections/featured",
inbox: ap_id <> "/inbox"
)
:ok
end
end
defp ingest_fixture!(rel_path) do
data = rel_path |> File.read!() |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
test "normalizes Link inReplyTo to a string" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_link.json")
assert object.data["inReplyTo"] == "https://fedidev.fun/assets/note1.jsonap"
end
test "normalizes list inReplyTo to the first object ID" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_list.json")
assert object.data["inReplyTo"] == "https://fedidev.fun/assets/note1.jsonap"
end
test "drops invalid map shapes in inReplyTo" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_collection.json")
assert object.data["inReplyTo"] == nil
end
test "accepts embedded objects in inReplyTo" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_embedded_note.json")
assert object.data["inReplyTo"] == "https://fedidev.fun/assets/note1.jsonap"
end
end

View file

@ -572,11 +572,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
object_with_reply = Map.put(data["object"], "inReplyTo", [])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == []
assert modified_object["inReplyTo"] == nil
end
test "returns modified object when allowed incoming reply", %{data: data} do