Merge pull request 'provide full replies collection in ActivityPub objects' (#904) from Oneric/akkoma:ap_replies_collection into develop

Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/904
This commit is contained in:
Oneric 2025-06-07 19:33:40 +00:00
commit 401883b230
15 changed files with 433 additions and 148 deletions

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Activity.Queries do
Contains queries for Activity.
"""
import Ecto.Query, only: [from: 2, where: 3]
import Ecto.Query, only: [from: 2]
@type query :: Ecto.Queryable.t() | Activity.t()
@ -72,22 +72,6 @@ def by_object_id(query, object_id) when is_binary(object_id) do
)
end
@spec by_object_in_reply_to_id(query, String.t(), keyword()) :: query
def by_object_in_reply_to_id(query, in_reply_to_id, opts \\ []) do
query =
if opts[:skip_preloading] do
Activity.with_joined_object(query)
else
Activity.with_preloaded_object(query)
end
where(
query,
[activity, object: o],
fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(in_reply_to_id))
)
end
@spec by_type(query, String.t()) :: query
def by_type(query \\ Activity, activity_type) do
from(

View file

@ -365,28 +365,6 @@ def local?(%Object{data: %{"id" => id}}) do
String.starts_with?(id, Pleroma.Web.Endpoint.url() <> "/")
end
def replies(object, opts \\ []) do
object = Object.normalize(object, fetch: false)
query =
Object
|> where(
[o],
fragment("(?)->>'inReplyTo' = ?", o.data, ^object.data["id"])
)
|> order_by([o], asc: o.id)
if opts[:self_only] do
actor = object.data["actor"]
where(query, [o], fragment("(?)->>'actor' = ?", o.data, ^actor))
else
query
end
end
def self_replies(object, opts \\ []),
do: replies(object, Keyword.put(opts, :self_only, true))
def tags(%Object{data: %{"tag" => tags}}) when is_list(tags), do: tags
def tags(_), do: []

View file

@ -94,13 +94,31 @@ defp cast_params(params) do
offset: :integer,
limit: :integer,
skip_extra_order: :boolean,
skip_order: :boolean
skip_order: :boolean,
order_asc: :boolean
}
params = Map.delete(params, :id_type)
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes
end
defp order_statement(query, table_binding, :asc) do
order_by(
query,
[{u, table_position(query, table_binding)}],
fragment("? asc nulls last", u.id)
)
end
defp order_statement(query, table_binding, :desc) do
order_by(
query,
[{u, table_position(query, table_binding)}],
fragment("? desc nulls last", u.id)
)
end
defp restrict(query, :min_id, %{min_id: min_id}, table_binding) do
where(query, [{q, table_position(query, table_binding)}], q.id > ^min_id)
end
@ -118,19 +136,16 @@ defp restrict(query, :order, %{skip_order: true}, _), do: query
defp restrict(%{order_bys: [_ | _]} = query, :order, %{skip_extra_order: true}, _), do: query
defp restrict(query, :order, %{min_id: _}, table_binding) do
order_by(
query,
[{u, table_position(query, table_binding)}],
fragment("? asc nulls last", u.id)
)
order_statement(query, table_binding, :asc)
end
defp restrict(query, :order, _options, table_binding) do
order_by(
query,
[{u, table_position(query, table_binding)}],
fragment("? desc nulls last", u.id)
)
defp restrict(query, :order, %{max_id: _}, table_binding) do
order_statement(query, table_binding, :desc)
end
defp restrict(query, :order, options, table_binding) do
dir = if options[:order_asc], do: :asc, else: :desc
order_statement(query, table_binding, dir)
end
defp restrict(query, :offset, %{offset: offset}, _table_binding) do
@ -150,11 +165,9 @@ defp restrict(query, :limit, options, _table_binding) do
defp restrict(query, _, _, _), do: query
defp enforce_order(result, %{min_id: _}) do
result
|> Enum.reverse()
end
defp enforce_order(result, %{min_id: _, order_asc: true}), do: result
defp enforce_order(result, %{min_id: _}), do: Enum.reverse(result)
defp enforce_order(result, %{max_id: _, order_asc: true}), do: Enum.reverse(result)
defp enforce_order(result, _), do: result
defp table_position(%Ecto.Query{} = query, binding_name) do

View file

@ -509,6 +509,28 @@ def fetch_activities_for_context(context, opts \\ %{}) do
|> Repo.all()
end
def fetch_objects_for_replies_collection(parent_ap_id, opts \\ %{}) do
opts =
opts
|> Map.put(:order_asc, true)
|> Map.put(:id_type, :integer)
from(o in Object,
where:
fragment("?->>'inReplyTo' = ?", o.data, ^parent_ap_id) and
fragment(
"(?->'to' \\? ?::text OR ?->'cc' \\? ?::text)",
o.data,
^Pleroma.Constants.as_public(),
o.data,
^Pleroma.Constants.as_public()
) and
fragment("?->>'type' <> 'Answer'", o.data),
select: %{id: o.id, ap_id: fragment("?->>'id'", o.data)}
)
|> Pagination.fetch_paginated(opts, :keyset)
end
@spec fetch_latest_direct_activity_id_for_context(String.t(), keyword() | map()) ::
FlakeId.Ecto.CompatType.t() | nil
def fetch_latest_direct_activity_id_for_context(context, opts \\ %{}) do

View file

@ -22,8 +22,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
alias Pleroma.Web.Plugs.FederatingPlug
require Logger
action_fallback(:errors)
@federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
@ -121,6 +119,35 @@ def object(%{assigns: assigns} = conn, _) do
end
end
def object_replies(%{assigns: assigns, query_params: params} = conn, _all_params) do
object_ap_id = conn.path_info |> Enum.reverse() |> tl() |> Enum.reverse()
object_ap_id = Endpoint.url() <> "/" <> Enum.join(object_ap_id, "/")
# Most other API params are converted to atoms by OpenAPISpex 3.x
# and therefore helper functions assume atoms. For consistency,
# also convert our params to atoms here.
params =
params
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
|> Map.put(:object_ap_id, object_ap_id)
|> Map.put(:order_asc, true)
|> Map.put(:conn, conn)
with %Object{} = object <- Object.get_cached_by_ap_id(object_ap_id),
user <- Map.get(assigns, :user, nil),
{_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
conn
|> maybe_skip_cache(user)
|> set_cache_ttl_for(object)
|> put_resp_content_type("application/activity+json")
|> put_view(ObjectView)
|> render("object_replies.json", render_params: params)
else
{:visible?, false} -> {:error, :not_found}
nil -> {:error, :not_found}
end
end
def track_object_fetch(conn, nil), do: conn
def track_object_fetch(conn, object_id) do
@ -287,8 +314,7 @@ def outbox(
|> put_view(UserView)
|> render("activity_collection_page.json", %{
activities: activities,
pagination: ControllerHelper.get_pagination_fields(conn, activities),
iri: "#{user.ap_id}/outbox"
pagination: ControllerHelper.get_pagination_fields(conn, activities)
})
end
end
@ -368,8 +394,7 @@ def read_inbox(
|> put_view(UserView)
|> render("activity_collection_page.json", %{
activities: activities,
pagination: ControllerHelper.get_pagination_fields(conn, activities),
iri: "#{user.ap_id}/inbox"
pagination: ControllerHelper.get_pagination_fields(conn, activities)
})
end

View file

@ -69,6 +69,7 @@ defp fix_tag(%{"tag" => tag} = data) when is_list(tag) do
defp fix_tag(%{"tag" => tag} = data) when is_map(tag), do: Map.put(data, "tag", [tag])
defp fix_tag(data), do: Map.drop(data, ["tag"])
# legacy internal *oma format
defp fix_replies(%{"replies" => replies} = data) when is_list(replies), do: data
defp fix_replies(%{"replies" => %{"first" => first}} = data) when is_binary(first) do
@ -85,9 +86,16 @@ defp fix_replies(%{"replies" => %{"first" => %{"items" => replies}}} = data)
when is_list(replies),
do: Map.put(data, "replies", replies)
defp fix_replies(%{"replies" => %{"first" => %{"orderedItems" => replies}}} = data)
when is_list(replies),
do: Map.put(data, "replies", replies)
defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies),
do: Map.put(data, "replies", replies)
defp fix_replies(%{"replies" => %{"orderedItems" => replies}} = data) when is_list(replies),
do: Map.put(data, "replies", replies)
defp fix_replies(data), do: Map.delete(data, "replies")
defp remote_mention_resolver(

View file

@ -22,8 +22,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.Federator
import Ecto.Query
require Pleroma.Constants
require Logger
@ -790,37 +788,22 @@ def set_quote_url(%{"quoteUri" => quote} = object) when is_binary(quote) do
def set_quote_url(obj), do: obj
@doc """
Serialized Mastodon-compatible `replies` collection containing _self-replies_.
Based on Mastodon's ActivityPub::NoteSerializer#replies.
Inline first page of the `replies` collection,
containing any replies in chronological order.
"""
def set_replies(obj_data) do
replies_uris =
with limit when limit > 0 <-
Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
%Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
object
|> Object.self_replies()
|> select([o], fragment("?->>'id'", o.data))
|> limit(^limit)
|> Repo.all()
else
_ -> []
end
set_replies(obj_data, replies_uris)
end
defp set_replies(obj, []) do
obj
end
defp set_replies(obj, replies_uris) do
replies_collection = %{
"type" => "Collection",
"items" => replies_uris
}
Map.merge(obj, %{"replies" => replies_collection})
with obj_ap_id when obj_ap_id != nil <- obj_data["id"],
limit when limit > 0 <-
Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
collection <-
Pleroma.Web.ActivityPub.ObjectView.render("object_replies.json", %{
render_params: %{object_ap_id: obj_data["id"], limit: limit, skip_ap_ctx: true}
}) do
Map.put(obj_data, "replies", collection)
else
0 -> Map.put(obj_data, "replies", obj_data["id"] <> "/replies")
_ -> obj_data
end
end
def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do

View file

@ -0,0 +1,59 @@
# Akkoma: Magically expressive social media
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# Copyright © 2025 Akkoma Authors <https://akkoma.dev/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.CollectionViewHelper do
alias Pleroma.Web.ActivityPub.Utils
def collection_page_offset(collection, iri, page, show_items \\ true, total \\ nil) do
offset = (page - 1) * 10
items = Enum.slice(collection, offset, 10)
items = Enum.map(items, fn user -> user.ap_id end)
total = total || length(collection)
map = %{
"id" => "#{iri}?page=#{page}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"totalItems" => total,
"orderedItems" => if(show_items, do: items, else: [])
}
if offset + 10 < total do
Map.put(map, "next", "#{iri}?page=#{page + 1}")
else
map
end
end
defp maybe_omit_next(pagination, _items, nil), do: pagination
defp maybe_omit_next(pagination, items, limit) when is_binary(limit) do
case Integer.parse(limit) do
{limit, ""} -> maybe_omit_next(pagination, items, limit)
_ -> maybe_omit_next(pagination, items, nil)
end
end
defp maybe_omit_next(pagination, items, limit) when is_number(limit) do
if Enum.count(items) < limit, do: Map.delete(pagination, "next"), else: pagination
end
def collection_page_keyset(
display_items,
pagination,
limit \\ nil,
skip_ap_context \\ false
) do
%{
"type" => "OrderedCollectionPage",
"orderedItems" => display_items
}
|> Map.merge(pagination)
|> maybe_omit_next(display_items, limit)
|> then(fn m ->
if skip_ap_context, do: m, else: Map.merge(m, Utils.make_json_ld_header())
end)
end
end

View file

@ -6,7 +6,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.CollectionViewHelper
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.ActivityPub
def render("object.json", %{object: %Object{} = object}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
@ -37,4 +40,89 @@ def render("object.json", %{object: %Activity{} = activity}) do
Map.merge(base, additional)
end
def render("object_replies.json", %{
conn: conn,
render_params: %{object_ap_id: object_ap_id, page: "true"} = params
}) do
params = Map.put_new(params, :limit, 40)
items = ActivityPub.fetch_objects_for_replies_collection(object_ap_id, params)
display_items = map_reply_collection_items(items)
pagination = ControllerHelper.get_pagination_fields(conn, items, %{}, :asc)
CollectionViewHelper.collection_page_keyset(display_items, pagination, params[:limit])
end
def render(
"object_replies.json",
%{
render_params: %{object_ap_id: object_ap_id} = params
} = opts
) do
params =
params
|> Map.drop([:max_id, :min_id, :since_id, :object_ap_id])
|> Map.put_new(:limit, 40)
|> Map.put(:total, true)
%{total: total, items: items} =
ActivityPub.fetch_objects_for_replies_collection(object_ap_id, params)
display_items = map_reply_collection_items(items)
first_pagination = reply_collection_first_pagination(items, opts)
col_ap =
%{
"id" => object_ap_id <> "/replies",
"type" => "OrderedCollection",
"totalItems" => total
}
col_ap =
if total > 0 do
first_page =
CollectionViewHelper.collection_page_keyset(
display_items,
first_pagination,
params[:limit],
true
)
Map.put(col_ap, "first", first_page)
else
col_ap
end
if params[:skip_ap_ctx] do
col_ap
else
Map.merge(col_ap, Pleroma.Web.ActivityPub.Utils.make_json_ld_header())
end
end
defp map_reply_collection_items(items), do: Enum.map(items, fn %{ap_id: ap_id} -> ap_id end)
defp reply_collection_first_pagination(items, %{conn: %Plug.Conn{} = conn}) do
ControllerHelper.get_pagination_fields(conn, items, %{"page" => true}, :asc)
end
defp reply_collection_first_pagination(items, %{render_params: %{object_ap_id: object_ap_id}}) do
%{
"id" => object_ap_id <> "/replies?page=true",
"partOf" => object_ap_id <> "/replies"
}
|> then(fn m ->
case items do
[] ->
m
i ->
next_id = object_ap_id <> "/replies?page=true&min_id=#{List.last(i)[:id]}"
Map.put(m, "next", next_id)
end
end)
end
end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.CollectionViewHelper
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
@ -141,7 +142,13 @@ def render("following.json", %{user: user, page: page} = opts) do
0
end
collection(following, "#{user.ap_id}/following", page, showing_items, total)
CollectionViewHelper.collection_page_offset(
following,
"#{user.ap_id}/following",
page,
showing_items,
total
)
|> Map.merge(Utils.make_json_ld_header())
end
@ -166,7 +173,12 @@ def render("following.json", %{user: user} = opts) do
"totalItems" => total,
"first" =>
if showing_items do
collection(following, "#{user.ap_id}/following", 1, !user.hide_follows)
CollectionViewHelper.collection_page_offset(
following,
"#{user.ap_id}/following",
1,
!user.hide_follows
)
else
"#{user.ap_id}/following?page=1"
end
@ -189,7 +201,13 @@ def render("followers.json", %{user: user, page: page} = opts) do
0
end
collection(followers, "#{user.ap_id}/followers", page, showing_items, total)
CollectionViewHelper.collection_page_offset(
followers,
"#{user.ap_id}/followers",
page,
showing_items,
total
)
|> Map.merge(Utils.make_json_ld_header())
end
@ -213,7 +231,13 @@ def render("followers.json", %{user: user} = opts) do
"type" => "OrderedCollection",
"first" =>
if showing_items do
collection(followers, "#{user.ap_id}/followers", 1, showing_items, total)
CollectionViewHelper.collection_page_offset(
followers,
"#{user.ap_id}/followers",
1,
showing_items,
total
)
else
"#{user.ap_id}/followers?page=1"
end
@ -233,22 +257,15 @@ def render("activity_collection.json", %{iri: iri}) do
def render("activity_collection_page.json", %{
activities: activities,
iri: iri,
pagination: pagination
}) do
collection =
display_items =
Enum.map(activities, fn activity ->
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
data
end)
%{
"type" => "OrderedCollectionPage",
"partOf" => iri,
"orderedItems" => collection
}
|> Map.merge(Utils.make_json_ld_header())
|> Map.merge(pagination)
CollectionViewHelper.collection_page_keyset(display_items, pagination)
end
def render("featured.json", %{
@ -276,27 +293,6 @@ defp maybe_put_total_items(map, true, total) do
Map.put(map, "totalItems", total)
end
def collection(collection, iri, page, show_items \\ true, total \\ nil) do
offset = (page - 1) * 10
items = Enum.slice(collection, offset, 10)
items = Enum.map(items, fn user -> user.ap_id end)
total = total || length(collection)
map = %{
"id" => "#{iri}?page=#{page}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"totalItems" => total,
"orderedItems" => if(show_items, do: items, else: [])
}
if offset < total do
Map.put(map, "next", "#{iri}?page=#{page + 1}")
else
map
end
end
defp maybe_make_image(func, key, user) do
image = func.(user, no_default: true)
maybe_insert_image(key, image)

View file

@ -54,34 +54,57 @@ def add_link_headers(conn, entries, extra_params) do
end
@id_keys Pagination.page_keys() -- ["limit", "order"]
defp build_pagination_fields(conn, min_id, max_id, extra_params) do
defp build_pagination_fields(conn, min_id, max_id, extra_params, order) do
params =
conn.params
|> Map.drop(Map.keys(conn.path_params) |> Enum.map(&String.to_existing_atom/1))
conn.body_params
|> Map.merge(conn.query_params)
|> Map.merge(extra_params)
|> Map.drop(@id_keys)
{{next_id, nid}, {prev_id, pid}} =
if order == :desc,
do: {{:max_id, max_id}, {:min_id, min_id}},
else: {{:min_id, min_id}, {:max_id, max_id}}
id = Phoenix.Controller.current_url(conn)
base_id = %{URI.parse(id) | query: nil} |> URI.to_string()
%{
"next" => current_url(conn, Map.put(params, :max_id, max_id)),
"prev" => current_url(conn, Map.put(params, :min_id, min_id)),
"id" => current_url(conn)
"next" => current_url(conn, Map.put(params, next_id, nid)),
"prev" => current_url(conn, Map.put(params, prev_id, pid)),
"id" => id,
"partOf" => base_id
}
end
def get_pagination_fields(conn, entries, extra_params \\ %{}) do
defp get_first_last_pagination_id(entries) do
case List.last(entries) do
%{pagination_id: max_id} when not is_nil(max_id) ->
%{pagination_id: min_id} = List.first(entries)
%{pagination_id: last_id} when not is_nil(last_id) ->
%{pagination_id: first_id} = List.first(entries)
{first_id, last_id}
build_pagination_fields(conn, min_id, max_id, extra_params)
%{id: max_id} ->
%{id: min_id} = List.first(entries)
build_pagination_fields(conn, min_id, max_id, extra_params)
%{id: last_id} ->
%{id: first_id} = List.first(entries)
{first_id, last_id}
_ ->
%{}
nil
end
end
def get_pagination_fields(conn, entries, extra_params \\ %{}, order \\ :desc)
def get_pagination_fields(conn, entries, extra_params, :desc) do
case get_first_last_pagination_id(entries) do
nil -> %{}
{min_id, max_id} -> build_pagination_fields(conn, min_id, max_id, extra_params, :desc)
end
end
def get_pagination_fields(conn, entries, extra_params, :asc) do
case get_first_last_pagination_id(entries) do
nil -> %{}
{max_id, min_id} -> build_pagination_fields(conn, min_id, max_id, extra_params, :asc)
end
end

View file

@ -824,6 +824,7 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/outbox", ActivityPubController, :outbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
get("/objects/:uuid/replies", ActivityPubController, :object_replies)
end
scope "/relay", Pleroma.Web.ActivityPub do

View file

@ -426,6 +426,75 @@ test "cached purged after object deletion", %{conn: conn} do
end
end
describe "/objects/:uuid/replies" do
test "it renders the top-level collection", %{
conn: conn
} do
user = insert(:user)
note = insert(:note_activity)
note = Pleroma.Activity.get_by_id_with_object(note.id)
uuid = String.split(note.object.data["id"], "/") |> List.last()
{:ok, _} =
CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: note.id})
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}/replies")
assert match?(
%{
"id" => _,
"type" => "OrderedCollection",
"totalItems" => 1,
"first" => %{
"id" => _,
"type" => "OrderedCollectionPage",
"orderedItems" => [_]
}
},
json_response(conn, 200)
)
end
test "it renders a collection page", %{
conn: conn
} do
user = insert(:user)
note = insert(:note_activity)
note = Pleroma.Activity.get_by_id_with_object(note.id)
uuid = String.split(note.object.data["id"], "/") |> List.last()
{:ok, r1} =
CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: note.id})
{:ok, r2} =
CommonAPI.post(user, %{status: "reply2", in_reply_to_status_id: note.id})
{:ok, _} =
CommonAPI.post(user, %{status: "reply3", in_reply_to_status_id: note.id})
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}/replies?page=true&min_id=#{r1.object.id}&limit=1")
expected_uris = [r2.object.data["id"]]
assert match?(
%{
"id" => _,
"type" => "OrderedCollectionPage",
"prev" => _,
"next" => _,
"orderedItems" => ^expected_uris
},
json_response(conn, 200)
)
end
end
describe "/activities/:uuid" do
test "it doesn't return a local-only activity", %{conn: conn} do
user = insert(:user)

View file

@ -681,12 +681,18 @@ test "returns object with emoji when object contains map tag" do
describe "set_replies/1" do
setup do: clear_config([:activitypub, :note_replies_output_limit], 2)
test "returns unmodified object if activity doesn't have self-replies" do
test "still provides reply collection id even if activity doesn't have replies yet" do
data = Jason.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
assert Transmogrifier.set_replies(data) == data
modified = Transmogrifier.set_replies(data)
refute data["replies"]
assert modified["replies"]
assert match?(%{"id" => "http" <> _, "totalItems" => 0}, modified["replies"])
# first page should be omitted if there are no entries anyway
refute modified["replies"]["first"]
end
test "sets `replies` collection with a limited number of self-replies" do
test "sets `replies` collection with a limited number of replies, preferring oldest" do
[user, another_user] = insert_list(2, :user)
{:ok, %{id: id1} = activity} = CommonAPI.post(user, %{status: "1"})
@ -715,7 +721,7 @@ test "sets `replies` collection with a limited number of self-replies" do
object = Object.normalize(activity, fetch: false)
replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end)
assert %{"type" => "Collection", "items" => ^replies_uris} =
assert %{"type" => "OrderedCollection", "first" => %{"orderedItems" => ^replies_uris}} =
Transmogrifier.set_replies(object.data)["replies"]
end
end

View file

@ -49,9 +49,39 @@ test "renders `replies` collection for a note activity" do
replies_uris = [self_reply1.object.data["id"]]
result = ObjectView.render("object.json", %{object: refresh_record(activity)})
assert %{"type" => "Collection", "items" => ^replies_uris} =
assert %{
"type" => "OrderedCollection",
"id" => _,
"first" => %{"orderedItems" => ^replies_uris}
} =
get_in(result, ["object", "replies"])
end
test "renders a replies collection on its own" do
user = insert(:user)
activity = insert(:note_activity, user: user)
activity = Pleroma.Activity.get_by_id_with_object(activity.id)
{:ok, r1} =
CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: activity.id})
{:ok, r2} =
CommonAPI.post(user, %{status: "reply1", in_reply_to_status_id: activity.id})
replies_uris = [r1.object.data["id"], r2.object.data["id"]]
result =
ObjectView.render("object_replies.json", %{
render_params: %{object_ap_id: activity.object.data["id"]}
})
%{
"type" => "OrderedCollection",
"id" => _,
"totalItems" => 2,
"first" => %{"orderedItems" => ^replies_uris}
} = result
end
end
test "renders a like activity" do