forked from AkkomaGang/akkoma
Merge branch '1505-threads-federation' into 'develop'
[#1505] Threads / replies federation Closes #1505 See merge request pleroma/pleroma!2129
This commit is contained in:
commit
1e1156b645
15 changed files with 366 additions and 25 deletions
|
@ -75,6 +75,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- A new users admin digest email
|
- A new users admin digest email
|
||||||
- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
|
- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
|
||||||
- Add an option `authorized_fetch_mode` to require HTTP signatures for AP fetches.
|
- Add an option `authorized_fetch_mode` to require HTTP signatures for AP fetches.
|
||||||
|
- ActivityPub: support for `replies` collection (output for outgoing federation & fetching on incoming federation).
|
||||||
<details>
|
<details>
|
||||||
<summary>API Changes</summary>
|
<summary>API Changes</summary>
|
||||||
|
|
||||||
|
@ -117,6 +118,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Tag feed: `/tags/:tag.rss` - list public statuses by hashtag.
|
- Tag feed: `/tags/:tag.rss` - list public statuses by hashtag.
|
||||||
- Mastodon API: Add `reacted` property to `emoji_reactions`
|
- Mastodon API: Add `reacted` property to `emoji_reactions`
|
||||||
- Pleroma API: Add reactions for a single emoji.
|
- Pleroma API: Add reactions for a single emoji.
|
||||||
|
- ActivityPub: `[:activitypub, :note_replies_output_limit]` setting sets the number of note self-replies to output on outgoing federation.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -328,6 +328,7 @@
|
||||||
unfollow_blocked: true,
|
unfollow_blocked: true,
|
||||||
outgoing_blocks: true,
|
outgoing_blocks: true,
|
||||||
follow_handshake_timeout: 500,
|
follow_handshake_timeout: 500,
|
||||||
|
note_replies_output_limit: 5,
|
||||||
sign_object_fetches: true,
|
sign_object_fetches: true,
|
||||||
authorized_fetch_mode: false
|
authorized_fetch_mode: false
|
||||||
|
|
||||||
|
@ -483,6 +484,7 @@
|
||||||
transmogrifier: 20,
|
transmogrifier: 20,
|
||||||
scheduled_activities: 10,
|
scheduled_activities: 10,
|
||||||
background: 5,
|
background: 5,
|
||||||
|
remote_fetcher: 2,
|
||||||
attachments_cleanup: 5,
|
attachments_cleanup: 5,
|
||||||
new_users_digest: 1
|
new_users_digest: 1
|
||||||
],
|
],
|
||||||
|
|
|
@ -662,7 +662,7 @@
|
||||||
label: "Fed. incoming replies max depth",
|
label: "Fed. incoming replies max depth",
|
||||||
type: :integer,
|
type: :integer,
|
||||||
description:
|
description:
|
||||||
"Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while" <>
|
"Max. depth of reply-to and reply activities fetching on incoming federation, to prevent out-of-memory situations while" <>
|
||||||
" fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.",
|
" fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.",
|
||||||
suggestions: [
|
suggestions: [
|
||||||
100
|
100
|
||||||
|
@ -1790,6 +1790,12 @@
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "Sign object fetches with HTTP signatures"
|
description: "Sign object fetches with HTTP signatures"
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
key: :note_replies_output_limit,
|
||||||
|
type: :integer,
|
||||||
|
description:
|
||||||
|
"The number of Note replies' URIs to be included with outgoing federation (`5` to match Mastodon hardcoded value, `0` to disable the output)."
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
key: :follow_handshake_timeout,
|
key: :follow_handshake_timeout,
|
||||||
type: :integer,
|
type: :integer,
|
||||||
|
|
|
@ -41,7 +41,7 @@ On the top right you will also see a wrench icon. This opens your personal setti
|
||||||
This is where the interesting stuff happens!
|
This is where the interesting stuff happens!
|
||||||
Depending on the timeline you will see different statuses, but each status has a standard structure:
|
Depending on the timeline you will see different statuses, but each status has a standard structure:
|
||||||
|
|
||||||
- Profile pic, name and link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the replied-to status). Clicking on the profile pic will uncollapse the user's profile.
|
- Profile pic, name and link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the reply-to status). Clicking on the profile pic will uncollapse the user's profile.
|
||||||
- A `+` button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime!
|
- A `+` button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime!
|
||||||
- An arrow icon allows you to open the status on the instance where it's originating from.
|
- An arrow icon allows you to open the status on the instance where it's originating from.
|
||||||
- The text of the status, including mentions and attachements. If you click on a mention, it will automatically open the profile page of that person.
|
- The text of the status, including mentions and attachements. If you click on a mention, it will automatically open the profile page of that person.
|
||||||
|
|
|
@ -7,7 +7,7 @@ defmodule Pleroma.Activity.Queries do
|
||||||
Contains queries for Activity.
|
Contains queries for Activity.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import Ecto.Query, only: [from: 2]
|
import Ecto.Query, only: [from: 2, where: 3]
|
||||||
|
|
||||||
@type query :: Ecto.Queryable.t() | Activity.t()
|
@type query :: Ecto.Queryable.t() | Activity.t()
|
||||||
|
|
||||||
|
@ -63,6 +63,22 @@ def by_object_id(query, object_id) when is_binary(object_id) do
|
||||||
)
|
)
|
||||||
end
|
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
|
@spec by_type(query, String.t()) :: query
|
||||||
def by_type(query \\ Activity, activity_type) do
|
def by_type(query \\ Activity, activity_type) do
|
||||||
from(
|
from(
|
||||||
|
|
|
@ -301,4 +301,26 @@ def update_data(%Object{data: data} = object, attrs \\ %{}) do
|
||||||
def local?(%Object{data: %{"id" => id}}) do
|
def local?(%Object{data: %{"id" => id}}) do
|
||||||
String.starts_with?(id, Pleroma.Web.base_url() <> "/")
|
String.starts_with?(id, Pleroma.Web.base_url() <> "/")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def replies(object, opts \\ []) do
|
||||||
|
object = Object.normalize(object)
|
||||||
|
|
||||||
|
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))
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Object.Fetcher do
|
||||||
alias Pleroma.Signature
|
alias Pleroma.Signature
|
||||||
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
|
alias Pleroma.Web.Federator
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
@ -59,20 +60,23 @@ def refetch_object(%Object{data: %{"id" => id}} = object) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO:
|
# Note: will create a Create activity, which we need internally at the moment.
|
||||||
# This will create a Create activity, which we need internally at the moment.
|
|
||||||
def fetch_object_from_id(id, options \\ []) do
|
def fetch_object_from_id(id, options \\ []) do
|
||||||
with {:fetch_object, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
|
with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
|
||||||
{:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
|
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
|
||||||
{:normalize, nil} <- {:normalize, Object.normalize(data, false)},
|
{_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
|
||||||
|
{_, nil} <- {:normalize, Object.normalize(data, false)},
|
||||||
params <- prepare_activity_params(data),
|
params <- prepare_activity_params(data),
|
||||||
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
|
{_, :ok} <- {:containment, Containment.contain_origin(id, params)},
|
||||||
{:transmogrifier, {:ok, activity}} <-
|
{_, {:ok, activity}} <-
|
||||||
{:transmogrifier, Transmogrifier.handle_incoming(params, options)},
|
{:transmogrifier, Transmogrifier.handle_incoming(params, options)},
|
||||||
{:object, _data, %Object{} = object} <-
|
{_, _data, %Object{} = object} <-
|
||||||
{:object, data, Object.normalize(activity, false)} do
|
{:object, data, Object.normalize(activity, false)} do
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
else
|
else
|
||||||
|
{:allowed_depth, false} ->
|
||||||
|
{:error, "Max thread distance exceeded."}
|
||||||
|
|
||||||
{:containment, _} ->
|
{:containment, _} ->
|
||||||
{:error, "Object containment failed."}
|
{:error, "Object containment failed."}
|
||||||
|
|
||||||
|
|
|
@ -156,8 +156,9 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
|
||||||
when not is_nil(in_reply_to) do
|
when not is_nil(in_reply_to) do
|
||||||
in_reply_to_id = prepare_in_reply_to(in_reply_to)
|
in_reply_to_id = prepare_in_reply_to(in_reply_to)
|
||||||
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
|
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
|
||||||
|
depth = (options[:depth] || 0) + 1
|
||||||
|
|
||||||
if Federator.allowed_incoming_reply_depth?(options[:depth]) do
|
if Federator.allowed_thread_distance?(depth) do
|
||||||
with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
|
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
|
%Activity{} = _ <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
|
||||||
object
|
object
|
||||||
|
@ -312,7 +313,7 @@ def fix_type(object, options \\ [])
|
||||||
|
|
||||||
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
|
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
|
||||||
when is_binary(reply_id) do
|
when is_binary(reply_id) do
|
||||||
with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
|
with true <- Federator.allowed_thread_distance?(options[:depth]),
|
||||||
{:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
|
{:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
|
||||||
Map.put(object, "type", "Answer")
|
Map.put(object, "type", "Answer")
|
||||||
else
|
else
|
||||||
|
@ -406,8 +407,7 @@ def handle_incoming(
|
||||||
|
|
||||||
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
|
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
|
||||||
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
|
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
|
||||||
options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
|
object = fix_object(object, options)
|
||||||
object = fix_object(data["object"], options)
|
|
||||||
|
|
||||||
params = %{
|
params = %{
|
||||||
to: data["to"],
|
to: data["to"],
|
||||||
|
@ -424,7 +424,20 @@ def handle_incoming(
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
ActivityPub.create(params)
|
with {:ok, created_activity} <- ActivityPub.create(params) do
|
||||||
|
reply_depth = (options[:depth] || 0) + 1
|
||||||
|
|
||||||
|
if Federator.allowed_thread_distance?(reply_depth) do
|
||||||
|
for reply_id <- replies(object) do
|
||||||
|
Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
|
||||||
|
"id" => reply_id,
|
||||||
|
"depth" => reply_depth
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, created_activity}
|
||||||
|
end
|
||||||
else
|
else
|
||||||
%Activity{} = activity -> {:ok, activity}
|
%Activity{} = activity -> {:ok, activity}
|
||||||
_e -> :error
|
_e -> :error
|
||||||
|
@ -442,7 +455,8 @@ def handle_incoming(
|
||||||
|> fix_addressing
|
|> fix_addressing
|
||||||
|
|
||||||
with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
|
with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
|
||||||
options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
|
reply_depth = (options[:depth] || 0) + 1
|
||||||
|
options = Keyword.put(options, :depth, reply_depth)
|
||||||
object = fix_object(object, options)
|
object = fix_object(object, options)
|
||||||
|
|
||||||
params = %{
|
params = %{
|
||||||
|
@ -903,6 +917,50 @@ def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_r
|
||||||
|
|
||||||
def set_reply_to_uri(obj), do: obj
|
def set_reply_to_uri(obj), do: obj
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
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
|
||||||
|
replies_collection = %{
|
||||||
|
"type" => "Collection",
|
||||||
|
"items" => replies_uris
|
||||||
|
}
|
||||||
|
|
||||||
|
Map.merge(obj, %{"replies" => replies_collection})
|
||||||
|
end
|
||||||
|
|
||||||
|
def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
|
||||||
|
items
|
||||||
|
end
|
||||||
|
|
||||||
|
def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
|
||||||
|
items
|
||||||
|
end
|
||||||
|
|
||||||
|
def replies(_), do: []
|
||||||
|
|
||||||
# Prepares the object of an outgoing create activity.
|
# Prepares the object of an outgoing create activity.
|
||||||
def prepare_object(object) do
|
def prepare_object(object) do
|
||||||
object
|
object
|
||||||
|
@ -914,6 +972,7 @@ def prepare_object(object) do
|
||||||
|> prepare_attachments
|
|> prepare_attachments
|
||||||
|> set_conversation
|
|> set_conversation
|
||||||
|> set_reply_to_uri
|
|> set_reply_to_uri
|
||||||
|
|> set_replies
|
||||||
|> strip_internal_fields
|
|> strip_internal_fields
|
||||||
|> strip_internal_tags
|
|> strip_internal_tags
|
||||||
|> set_type
|
|> set_type
|
||||||
|
|
|
@ -15,13 +15,19 @@ defmodule Pleroma.Web.Federator do
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@doc "Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161)"
|
@doc """
|
||||||
|
Returns `true` if the distance to target object does not exceed max configured value.
|
||||||
|
Serves to prevent fetching of very long threads, especially useful on smaller instances.
|
||||||
|
Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161).
|
||||||
|
Applies to fetching of both ancestor (reply-to) and child (reply) objects.
|
||||||
|
"""
|
||||||
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
|
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
|
||||||
def allowed_incoming_reply_depth?(depth) do
|
def allowed_thread_distance?(distance) do
|
||||||
max_replies_depth = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth])
|
max_distance = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth])
|
||||||
|
|
||||||
if max_replies_depth do
|
if max_distance && max_distance >= 0 do
|
||||||
(depth || 1) <= max_replies_depth
|
# Default depth is 0 (an object has zero distance from itself in its thread)
|
||||||
|
(distance || 0) <= max_distance
|
||||||
else
|
else
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
20
lib/pleroma/workers/remote_fetcher_worker.ex
Normal file
20
lib/pleroma/workers/remote_fetcher_worker.ex
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Workers.RemoteFetcherWorker do
|
||||||
|
alias Pleroma.Object.Fetcher
|
||||||
|
|
||||||
|
use Pleroma.Workers.WorkerHelper, queue: "remote_fetcher"
|
||||||
|
|
||||||
|
@impl Oban.Worker
|
||||||
|
def perform(
|
||||||
|
%{
|
||||||
|
"op" => "fetch_remote",
|
||||||
|
"id" => id
|
||||||
|
} = args,
|
||||||
|
_job
|
||||||
|
) do
|
||||||
|
{:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"])
|
||||||
|
end
|
||||||
|
end
|
13
test/fixtures/mastodon-post-activity.json
vendored
13
test/fixtures/mastodon-post-activity.json
vendored
|
@ -35,6 +35,19 @@
|
||||||
"inReplyTo": null,
|
"inReplyTo": null,
|
||||||
"inReplyToAtomUri": null,
|
"inReplyToAtomUri": null,
|
||||||
"published": "2018-02-12T14:08:20Z",
|
"published": "2018-02-12T14:08:20Z",
|
||||||
|
"replies": {
|
||||||
|
"id": "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies",
|
||||||
|
"type": "Collection",
|
||||||
|
"first": {
|
||||||
|
"type": "CollectionPage",
|
||||||
|
"next": "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies?min_id=99512778738411824&page=true",
|
||||||
|
"partOf": "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies",
|
||||||
|
"items": [
|
||||||
|
"http://mastodon.example.org/users/admin/statuses/99512778738411823",
|
||||||
|
"http://mastodon.example.org/users/admin/statuses/99512778738411824"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"sensitive": true,
|
"sensitive": true,
|
||||||
"summary": "cw",
|
"summary": "cw",
|
||||||
"tag": [
|
"tag": [
|
||||||
|
|
|
@ -26,6 +26,31 @@ defmodule Pleroma.Object.FetcherTest do
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "max thread distance restriction" do
|
||||||
|
@ap_id "http://mastodon.example.org/@admin/99541947525187367"
|
||||||
|
|
||||||
|
clear_config([:instance, :federation_incoming_replies_max_depth])
|
||||||
|
|
||||||
|
test "it returns thread depth exceeded error if thread depth is exceeded" do
|
||||||
|
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
|
||||||
|
|
||||||
|
assert {:error, "Max thread distance exceeded."} =
|
||||||
|
Fetcher.fetch_object_from_id(@ap_id, depth: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do
|
||||||
|
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
|
||||||
|
|
||||||
|
assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it fetches object if requested depth does not exceed max thread depth" do
|
||||||
|
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 10)
|
||||||
|
|
||||||
|
assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "actor origin containment" do
|
describe "actor origin containment" do
|
||||||
test "it rejects objects with a bogus origin" do
|
test "it rejects objects with a bogus origin" do
|
||||||
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
|
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
|
||||||
|
|
|
@ -9,6 +9,10 @@ defmodule Pleroma.Tests.ObanHelpers do
|
||||||
|
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
|
|
||||||
|
def wipe_all do
|
||||||
|
Repo.delete_all(Oban.Job)
|
||||||
|
end
|
||||||
|
|
||||||
def perform_all do
|
def perform_all do
|
||||||
Oban.Job
|
Oban.Job
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
|
defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
|
||||||
|
use Oban.Testing, repo: Pleroma.Repo
|
||||||
use Pleroma.DataCase
|
use Pleroma.DataCase
|
||||||
|
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Object.Fetcher
|
alias Pleroma.Object.Fetcher
|
||||||
|
@ -40,7 +42,7 @@ test "it ignores an incoming notice if we already have it" do
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag capture_log: true
|
@tag capture_log: true
|
||||||
test "it fetches replied-to activities if we don't have them" do
|
test "it fetches reply-to activities if we don't have them" do
|
||||||
data =
|
data =
|
||||||
File.read!("test/fixtures/mastodon-post-activity.json")
|
File.read!("test/fixtures/mastodon-post-activity.json")
|
||||||
|> Poison.decode!()
|
|> Poison.decode!()
|
||||||
|
@ -61,7 +63,7 @@ test "it fetches replied-to activities if we don't have them" do
|
||||||
assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
|
assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not fetch replied-to activities beyond max_replies_depth" do
|
test "it does not fetch reply-to activities beyond max replies depth limit" do
|
||||||
data =
|
data =
|
||||||
File.read!("test/fixtures/mastodon-post-activity.json")
|
File.read!("test/fixtures/mastodon-post-activity.json")
|
||||||
|> Poison.decode!()
|
|> Poison.decode!()
|
||||||
|
@ -73,7 +75,7 @@ test "it does not fetch replied-to activities beyond max_replies_depth" do
|
||||||
data = Map.put(data, "object", object)
|
data = Map.put(data, "object", object)
|
||||||
|
|
||||||
with_mock Pleroma.Web.Federator,
|
with_mock Pleroma.Web.Federator,
|
||||||
allowed_incoming_reply_depth?: fn _ -> false end do
|
allowed_thread_distance?: fn _ -> false end do
|
||||||
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
|
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
|
||||||
|
|
||||||
returned_object = Object.normalize(returned_activity, false)
|
returned_object = Object.normalize(returned_activity, false)
|
||||||
|
@ -1348,6 +1350,101 @@ test "it accepts Move activities" do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "`handle_incoming/2`, Mastodon format `replies` handling" do
|
||||||
|
clear_config([:activitypub, :note_replies_output_limit]) do
|
||||||
|
Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5)
|
||||||
|
end
|
||||||
|
|
||||||
|
clear_config([:instance, :federation_incoming_replies_max_depth])
|
||||||
|
|
||||||
|
setup do
|
||||||
|
data =
|
||||||
|
"test/fixtures/mastodon-post-activity.json"
|
||||||
|
|> File.read!()
|
||||||
|
|> Poison.decode!()
|
||||||
|
|
||||||
|
items = get_in(data, ["object", "replies", "first", "items"])
|
||||||
|
assert length(items) > 0
|
||||||
|
|
||||||
|
%{data: data, items: items}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "schedules background fetching of `replies` items if max thread depth limit allows", %{
|
||||||
|
data: data,
|
||||||
|
items: items
|
||||||
|
} do
|
||||||
|
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 10)
|
||||||
|
|
||||||
|
{:ok, _activity} = Transmogrifier.handle_incoming(data)
|
||||||
|
|
||||||
|
for id <- items do
|
||||||
|
job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
|
||||||
|
assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
|
||||||
|
%{data: data} do
|
||||||
|
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
|
||||||
|
|
||||||
|
{:ok, _activity} = Transmogrifier.handle_incoming(data)
|
||||||
|
|
||||||
|
assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "`handle_incoming/2`, Pleroma format `replies` handling" do
|
||||||
|
clear_config([:activitypub, :note_replies_output_limit]) do
|
||||||
|
Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5)
|
||||||
|
end
|
||||||
|
|
||||||
|
clear_config([:instance, :federation_incoming_replies_max_depth])
|
||||||
|
|
||||||
|
setup do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{"status" => "post1"})
|
||||||
|
|
||||||
|
{:ok, reply1} =
|
||||||
|
CommonAPI.post(user, %{"status" => "reply1", "in_reply_to_status_id" => activity.id})
|
||||||
|
|
||||||
|
{:ok, reply2} =
|
||||||
|
CommonAPI.post(user, %{"status" => "reply2", "in_reply_to_status_id" => activity.id})
|
||||||
|
|
||||||
|
replies_uris = Enum.map([reply1, reply2], fn a -> a.object.data["id"] end)
|
||||||
|
|
||||||
|
{:ok, federation_output} = Transmogrifier.prepare_outgoing(activity.data)
|
||||||
|
|
||||||
|
Repo.delete(activity.object)
|
||||||
|
Repo.delete(activity)
|
||||||
|
|
||||||
|
%{federation_output: federation_output, replies_uris: replies_uris}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "schedules background fetching of `replies` items if max thread depth limit allows", %{
|
||||||
|
federation_output: federation_output,
|
||||||
|
replies_uris: replies_uris
|
||||||
|
} do
|
||||||
|
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 1)
|
||||||
|
|
||||||
|
{:ok, _activity} = Transmogrifier.handle_incoming(federation_output)
|
||||||
|
|
||||||
|
for id <- replies_uris do
|
||||||
|
job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
|
||||||
|
assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
|
||||||
|
%{federation_output: federation_output} do
|
||||||
|
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
|
||||||
|
|
||||||
|
{:ok, _activity} = Transmogrifier.handle_incoming(federation_output)
|
||||||
|
|
||||||
|
assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "prepare outgoing" do
|
describe "prepare outgoing" do
|
||||||
test "it inlines private announced objects" do
|
test "it inlines private announced objects" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
@ -2046,4 +2143,49 @@ test "returns object with emoji when object contains map tag" do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "set_replies/1" do
|
||||||
|
clear_config([:activitypub, :note_replies_output_limit]) do
|
||||||
|
Pleroma.Config.put([:activitypub, :note_replies_output_limit], 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns unmodified object if activity doesn't have self-replies" do
|
||||||
|
data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
|
||||||
|
assert Transmogrifier.set_replies(data) == data
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sets `replies` collection with a limited number of self-replies" do
|
||||||
|
[user, another_user] = insert_list(2, :user)
|
||||||
|
|
||||||
|
{: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)
|
||||||
|
replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end)
|
||||||
|
|
||||||
|
assert %{"type" => "Collection", "items" => ^replies_uris} =
|
||||||
|
Transmogrifier.set_replies(object.data)["replies"]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -36,6 +36,26 @@ test "renders a note activity" do
|
||||||
assert result["@context"]
|
assert result["@context"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "note activity's `replies` collection rendering" do
|
||||||
|
clear_config([:activitypub, :note_replies_output_limit]) do
|
||||||
|
Pleroma.Config.put([:activitypub, :note_replies_output_limit], 5)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders `replies` collection for a note activity" do
|
||||||
|
user = insert(:user)
|
||||||
|
activity = insert(:note_activity, user: user)
|
||||||
|
|
||||||
|
{:ok, self_reply1} =
|
||||||
|
CommonAPI.post(user, %{"status" => "self-reply 1", "in_reply_to_status_id" => activity.id})
|
||||||
|
|
||||||
|
replies_uris = [self_reply1.object.data["id"]]
|
||||||
|
result = ObjectView.render("object.json", %{object: refresh_record(activity)})
|
||||||
|
|
||||||
|
assert %{"type" => "Collection", "items" => ^replies_uris} =
|
||||||
|
get_in(result, ["object", "replies"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "renders a like activity" do
|
test "renders a like activity" do
|
||||||
note = insert(:note_activity)
|
note = insert(:note_activity)
|
||||||
object = Object.normalize(note)
|
object = Object.normalize(note)
|
||||||
|
|
Loading…
Reference in a new issue