Treat known quotes and replies as such even if parent unavailable
All checks were successful
ci/woodpecker/pr/test/1 Pipeline was successful
ci/woodpecker/pr/test/2 Pipeline was successful

Happens commonly for e.g. replies to follower-only posts
if no one one your instance follows the replied-to account
or replies/quotes of deleted posts.
Before this change Masto API response would treat those
replies as root posts, making it hard to automatically or
mentally filter them out.

With this change replies already show up sensibly as
recognisable  replies in akkoma-fe.
Quotes of unavailable posts however still show up as if they
weren’t quotes at all, but this can only be improved client-side.

Fixes: #715
This commit is contained in:
Oneric 2025-10-11 00:00:00 +00:00
commit 0907521971
4 changed files with 82 additions and 10 deletions

View file

@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
### REMOVED
### Added
- status responses include two new fields for ActivityPub cross-referencing: `akkoma.quote_apid` and `akkoma.in_reply_to_apid`
### Fixed
- replies and quotes to unresolvable posts now fill out IDs for replied to
status, user or quoted status with a 404-ing ID to make them recognisable as
replies/quotes instead of pretending theyre root posts
### Changed
## 2025.10

View file

@ -250,6 +250,16 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
}
}
]
},
in_reply_to_apid: %Schema{
type: :string,
nullable: true,
description: "The AcitivityPub ID this post is replying to, if it is a reply."
},
quote_apid: %Schema{
type: :string,
nullable: true,
description: "The AcitivityPub ID this post is quoting, if it is a quote."
}
}
},

View file

@ -26,6 +26,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
# Used as a placeholder to represent known-existing relatives we do cannot resolve locally
# will always 404 when supplied to API endpoints
@ghost_flake_id "_"
defp fetch_rich_media_for_activities(activities) do
Enum.each(activities, fn activity ->
Card.get_by_activity(activity)
@ -277,9 +281,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
nil
end
reply_to = get_reply_to(activity, opts)
reply_to_apid = get_single_apid(object.data, "inReplyTo")
reply_to = reply_to_apid && get_reply_to(activity, opts)
reply_to_id = reply_to_apid && get_id_or_ghost(reply_to)
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
reply_to_user_id = reply_to_apid && get_id_or_ghost(reply_to_user)
history_len =
1 +
@ -363,7 +370,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
{pinned?, pinned_at} = pin_data(object, user)
quote = Activity.get_quoted_activity_from_object(object)
quote_apid = get_single_apid(object.data, "quoteUri")
quote = quote_apid && Activity.get_quoted_activity_from_object(object)
quote_id = quote_apid && get_id_or_ghost(quote)
lang = language(object)
%{
@ -375,8 +385,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
user: user,
for: opts[:for]
}),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
in_reply_to_id: reply_to_id,
in_reply_to_account_id: reply_to_user_id,
reblog: nil,
card: card,
content: content_html,
@ -401,7 +411,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
application: build_application(object.data["generator"]),
language: lang,
emojis: build_emojis(object.data["emoji"]),
quote_id: if(quote, do: quote.id, else: nil),
quote_id: quote_id,
quote: maybe_render_quote(quote, opts),
emoji_reactions: emoji_reactions,
pleroma: %{
@ -419,7 +429,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
pinned_at: pinned_at
},
akkoma: %{
source: object.data["source"]
source: object.data["source"],
# Note: these AP IDs will also be filled out if we cannot resolve the actual object
# (e.g. because its a private post we aren't allowed to access, or just federation woes)
# allowing users to potentially discover the full context from other accounts/servers.
in_reply_to_apid: reply_to_apid,
quote_apid: quote_apid
}
}
else
@ -637,6 +652,26 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
end
defp get_id_or_ghost(object) do
(object && to_string(object.id)) || @ghost_flake_id
end
defp get_single_apid(object, key) do
apid = object[key]
apid =
case apid do
[head | _] -> head
_ -> apid
end
if apid != "" and is_binary(apid) do
apid
else
nil
end
end
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
object = Object.normalize(activity, fetch: false)

View file

@ -326,7 +326,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
pinned_at: nil
},
akkoma: %{
source: HTML.filter_tags(object_data["content"])
source: HTML.filter_tags(object_data["content"]),
in_reply_to_apid: nil,
quote_apid: nil
},
quote_id: nil,
quote: nil
@ -417,6 +419,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
assert status.in_reply_to_id == to_string(note.id)
end
test "a reply to an unavailable post" do
note = insert(:note, data: %{"inReplyTo" => "https://example.org/404"})
activity = insert(:note_activity, note: note)
status = StatusView.render("show.json", %{activity: activity})
assert status.in_reply_to_id == "_"
assert status.in_reply_to_account_id == "_"
assert status.akkoma.in_reply_to_apid == "https://example.org/404"
end
test "a quote" do
note = insert(:note_activity)
user = insert(:user)
@ -433,12 +446,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
end
test "a quote that we can't resolve" do
note = insert(:note_activity, quoteUri: "oopsie")
note = insert(:note, data: %{"quoteUri" => "oopsie"})
activity = insert(:note_activity, note: note)
status = StatusView.render("show.json", %{activity: note})
status = StatusView.render("show.json", %{activity: activity})
assert is_nil(status.quote_id)
assert is_nil(status.quote)
assert status.quote_id == "_"
assert status.akkoma.quote_apid == "oopsie"
end
test "a quote from a user we block" do