mirror of
https://git.pleroma.social/pleroma/pleroma.git
synced 2025-04-09 12:34:09 +00:00
Merge branch 'replies-collection' into 'develop'
Draft: Support replies collection for compatibility with Mastodon reply-fetching See merge request pleroma/pleroma!4340
This commit is contained in:
commit
870f5890f8
8 changed files with 158 additions and 83 deletions
1
changelog.d/replies-collection.add
Normal file
1
changelog.d/replies-collection.add
Normal file
|
@ -0,0 +1 @@
|
|||
Support replies collection for compatibility with Mastodon reply-fetching
|
|
@ -401,28 +401,6 @@ defmodule Pleroma.Object 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: []
|
||||
|
|
|
@ -501,6 +501,32 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> Repo.all()
|
||||
end
|
||||
|
||||
def fetch_replies(%{data: %{"actor" => actor, "id" => ap_id}}, opts) do
|
||||
public = [Constants.as_public()]
|
||||
|
||||
recipients =
|
||||
if opts[:user],
|
||||
do: [opts[:user].ap_id | User.following(opts[:user])] ++ public,
|
||||
else: public
|
||||
|
||||
from(activity in Activity)
|
||||
|> maybe_preload_objects(opts)
|
||||
|> maybe_preload_bookmarks(opts)
|
||||
|> maybe_set_thread_muted_field(opts)
|
||||
|> Activity.Queries.by_object_in_reply_to_id(ap_id)
|
||||
|> restrict_type(%{type: "Create"})
|
||||
|> restrict_author(actor, opts)
|
||||
|> restrict_unauthenticated(opts[:user])
|
||||
|> restrict_blocked(opts)
|
||||
|> restrict_blockers_visibility(opts)
|
||||
|> restrict_recipients(recipients, opts[:user])
|
||||
|> restrict_filtered(opts)
|
||||
|> exclude_poll_votes(opts)
|
||||
|> exclude_id(opts)
|
||||
|> order_by([activity], desc: activity.id)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec fetch_latest_direct_activity_id_for_context(String.t(), keyword() | map()) ::
|
||||
Ecto.UUID.t() | nil
|
||||
def fetch_latest_direct_activity_id_for_context(context, opts \\ %{}) do
|
||||
|
@ -1299,6 +1325,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp restrict_rule(query, _), do: query
|
||||
|
||||
defp restrict_author(query, actor, %{only_self: true}) do
|
||||
where(query, [o], fragment("(?)->>'actor' = ?", o.data, ^actor))
|
||||
end
|
||||
|
||||
defp restrict_author(query, actor, %{only_other_accounts: true}) do
|
||||
where(query, [o], fragment("(?)->>'actor' != ?", o.data, ^actor))
|
||||
end
|
||||
|
||||
defp restrict_author(query, _, _), do: query
|
||||
|
||||
defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
|
||||
|
||||
defp exclude_poll_votes(query, _) do
|
||||
|
|
|
@ -96,6 +96,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
end
|
||||
|
||||
def replies(%{assigns: assigns} = conn, %{"page" => page?} = params)
|
||||
when page? in [true, "true"] do
|
||||
with only_other_accounts? <- Map.get(params, "only_other_accounts", false),
|
||||
ap_id <- (Endpoint.url() <> conn.request_path) |> String.trim_trailing("/replies"),
|
||||
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
|
||||
user <- Map.get(assigns, :user, nil),
|
||||
{_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> put_view(ObjectView)
|
||||
|> render("replies_collection_page.json", %{
|
||||
user: user,
|
||||
object: object,
|
||||
only_other_accounts: only_other_accounts?,
|
||||
iri: "#{object.data["id"]}/replies"
|
||||
})
|
||||
else
|
||||
{:visible?, false} -> {:error, :not_found}
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def replies(%{assigns: assigns} = conn, _) do
|
||||
with ap_id <-
|
||||
(Endpoint.url() <> conn.request_path)
|
||||
|> String.trim_trailing("/replies"),
|
||||
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
|
||||
user <- Map.get(assigns, :user, nil),
|
||||
{_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> put_view(ObjectView)
|
||||
|> render("replies_collection.json", %{
|
||||
user: user,
|
||||
object: object,
|
||||
iri: "#{object.data["id"]}/replies"
|
||||
})
|
||||
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
|
||||
|
|
|
@ -22,7 +22,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Federator
|
||||
|
||||
import Ecto.Query
|
||||
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||
|
||||
require Pleroma.Constants
|
||||
|
@ -724,36 +723,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
Serialized Mastodon-compatible `replies` collection containing _self-replies_.
|
||||
Based on Mastodon's ActivityPub::NoteSerializer#replies.
|
||||
"""
|
||||
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
|
||||
def set_replies(%{"id" => object_id} = object) do
|
||||
replies_collection = %{
|
||||
"id" => object_id <> "/replies",
|
||||
"type" => "Collection",
|
||||
"items" => replies_uris
|
||||
"first" => object_id <> "/replies?only_other_accounts=false&page=true"
|
||||
}
|
||||
|
||||
Map.merge(obj, %{"replies" => replies_collection})
|
||||
Map.merge(object, %{"replies" => replies_collection})
|
||||
end
|
||||
|
||||
def set_replies(object), do: object
|
||||
|
||||
def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
|
||||
items
|
||||
end
|
||||
|
|
|
@ -7,9 +7,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
||||
def render("object.json", %{object: %Object{} = object}) do
|
||||
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(object.data)
|
||||
base = Utils.make_json_ld_header(object.data)
|
||||
|
||||
additional = Transmogrifier.prepare_object(object.data)
|
||||
Map.merge(base, additional)
|
||||
|
@ -17,7 +18,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
|
|||
|
||||
def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity})
|
||||
when activity_type in ["Create", "Listen"] do
|
||||
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data)
|
||||
base = Utils.make_json_ld_header(activity.data)
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
additional =
|
||||
|
@ -28,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
|
|||
end
|
||||
|
||||
def render("object.json", %{object: %Activity{} = activity}) do
|
||||
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data)
|
||||
base = Utils.make_json_ld_header(activity.data)
|
||||
object_id = Object.normalize(activity, id_only: true)
|
||||
|
||||
additional =
|
||||
|
@ -37,4 +38,61 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
|
|||
|
||||
Map.merge(base, additional)
|
||||
end
|
||||
|
||||
def render("replies_collection.json", %{user: user, object: object, iri: iri}) do
|
||||
%{
|
||||
"id" => iri,
|
||||
"type" => "Collection",
|
||||
"first" =>
|
||||
render("replies_collection_page.json", %{
|
||||
user: user,
|
||||
object: object,
|
||||
only_other_accounts: false,
|
||||
iri: iri
|
||||
})
|
||||
|> Map.drop(["@context"])
|
||||
}
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
def render("replies_collection_page.json", %{
|
||||
user: user,
|
||||
object: object,
|
||||
only_other_accounts: only_other_accounts,
|
||||
iri: iri
|
||||
}) do
|
||||
replies =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_replies(object, %{
|
||||
user: user,
|
||||
only_other_accounts: only_other_accounts,
|
||||
only_self: !only_other_accounts
|
||||
})
|
||||
|
||||
collection =
|
||||
Enum.map(replies, fn
|
||||
%{local: true} = activity ->
|
||||
Transmogrifier.prepare_object(activity.object.data)
|
||||
|
||||
activity ->
|
||||
activity.object.data["id"]
|
||||
end)
|
||||
|
||||
next =
|
||||
if !only_other_accounts do
|
||||
%{
|
||||
"next" => iri <> "?only_other_accounts=true&page=true"
|
||||
}
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
||||
%{
|
||||
"id" => iri <> "?only_other_accounts=#{only_other_accounts}&page=true",
|
||||
"type" => "CollectionPage",
|
||||
"partOf" => iri,
|
||||
"items" => collection
|
||||
}
|
||||
|> Map.merge(next)
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
end
|
||||
|
|
|
@ -943,6 +943,8 @@ defmodule Pleroma.Web.Router do
|
|||
get("/users/:nickname/followers", ActivityPubController, :followers)
|
||||
get("/users/:nickname/following", ActivityPubController, :following)
|
||||
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
|
||||
|
||||
get("/objects/:id/replies", ActivityPubController, :replies)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web.ActivityPub do
|
||||
|
|
|
@ -696,41 +696,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest 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
|
||||
data = Jason.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
|
||||
assert Transmogrifier.set_replies(data) == data
|
||||
end
|
||||
test "sets `replies` to a link to a collection page" do
|
||||
object = insert(:note) |> IO.inspect()
|
||||
|
||||
test "sets `replies` collection with a limited number of self-replies" do
|
||||
[user, another_user] = insert_list(2, :user)
|
||||
replies_url = object.data["id"] <> "/replies"
|
||||
replies_page = replies_url <> "?only_other_accounts=false&page=true"
|
||||
|
||||
{:ok, %{id: id1} = activity} = CommonAPI.post(user, %{status: "1"})
|
||||
|
||||
{:ok, %{id: id2} = self_reply1} =
|
||||
CommonAPI.post(user, %{status: "self-reply 1", in_reply_to_status_id: id1})
|
||||
|
||||
{:ok, self_reply2} =
|
||||
CommonAPI.post(user, %{status: "self-reply 2", in_reply_to_status_id: id1})
|
||||
|
||||
# Assuming to _not_ be present in `replies` due to :note_replies_output_limit is set to 2
|
||||
{:ok, _} = CommonAPI.post(user, %{status: "self-reply 3", in_reply_to_status_id: id1})
|
||||
|
||||
{:ok, _} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "self-reply to self-reply",
|
||||
in_reply_to_status_id: id2
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
CommonAPI.post(another_user, %{
|
||||
status: "another user's reply",
|
||||
in_reply_to_status_id: id1
|
||||
})
|
||||
|
||||
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" => "Collection",
|
||||
"id" => ^replies_url,
|
||||
"first" => ^replies_page
|
||||
} =
|
||||
Transmogrifier.set_replies(object.data)["replies"]
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue