Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
24 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index af6aa0781..0bce5df6b 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -1,279 +1,295 @@
# 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.Publisher do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Delivery
alias Pleroma.HTTP
alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
require Pleroma.Constants
import Pleroma.Web.ActivityPub.Visibility
@behaviour Pleroma.Web.Federator.Publisher
require Logger
@moduledoc """
ActivityPub outgoing federation module.
"""
@doc """
Determine if an activity can be represented by running it through Transmogrifier.
"""
def is_representable?(%Activity{} = activity) do
with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
true
else
_e ->
false
end
end
@doc """
Publish a single message to a peer. Takes a struct with the following
parameters set:
* `inbox`: the inbox to publish to
* `json`: the JSON message body representing the ActivityPub message
* `actor`: the actor which is signing the message
* `id`: the ActivityStreams URI of the message
"""
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
Logger.debug("Federating #{id} to #{inbox}")
uri = %{path: path} = URI.parse(inbox)
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date = Pleroma.Signature.signed_date()
signature =
Pleroma.Signature.sign(actor, %{
"(request-target)": "post #{path}",
host: signature_host(uri),
"content-length": byte_size(json),
digest: digest,
date: date
})
with {:ok, %{status: code}} = result when code in 200..299 <-
HTTP.post(
inbox,
json,
[
{"Content-Type", "application/activity+json"},
{"Date", date},
{"signature", signature},
{"digest", digest}
]
) do
if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do
Instances.set_reachable(inbox)
end
result
else
{_post_result, response} ->
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
{:error, response}
end
end
def publish_one(%{actor_id: actor_id} = params) do
actor = User.get_cached_by_id(actor_id)
params
|> Map.delete(:actor_id)
|> Map.put(:actor, actor)
|> publish_one()
end
defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
if port == URI.default_port(scheme) do
host
else
"#{host}:#{port}"
end
end
defp should_federate?(inbox, public) do
if public do
true
else
%{host: host} = URI.parse(inbox)
quarantined_instances =
Config.get([:instance, :quarantined_instances], [])
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
!Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
end
end
@spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
defp recipients(actor, activity) do
followers =
if actor.follower_address in activity.recipients do
User.get_external_followers(actor)
else
[]
end
fetchers =
with %Activity{data: %{"type" => "Delete"}} <- activity,
%Object{id: object_id} <- Object.normalize(activity, fetch: false),
fetchers <- User.get_delivered_users_by_object_id(object_id),
_ <- Delivery.delete_all_by_object_id(object_id) do
fetchers
else
_ ->
[]
end
Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
end
defp get_cc_ap_ids(ap_id, recipients) do
host = Map.get(URI.parse(ap_id), :host)
recipients
|> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
|> Enum.map(& &1.ap_id)
end
defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox
defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox
@doc """
Determine a user inbox to use based on heuristics. These heuristics
are based on an approximation of the ``sharedInbox`` rules in the
[ActivityPub specification][ap-sharedinbox].
Please do not edit this function (or its children) without reading
the spec, as editing the code is likely to introduce some breakage
without some familiarity.
[ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
"""
def determine_inbox(
%Activity{data: activity_data},
%User{inbox: inbox} = user
) do
to = activity_data["to"] || []
cc = activity_data["cc"] || []
type = activity_data["type"]
cond do
type == "Delete" ->
maybe_use_sharedinbox(user)
Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
maybe_use_sharedinbox(user)
length(to) + length(cc) > 1 ->
maybe_use_sharedinbox(user)
true ->
inbox
end
end
@doc """
Publishes an activity with BCC to all relevant peers.
"""
- def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
+ def publish(%User{} = orig_actor, %{data: %{"bcc" => bcc}} = activity)
when is_list(bcc) and bcc != [] do
public = is_public?(activity)
- {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, actor, activity, data} = prepare_activity(orig_actor, activity)
recipients = recipients(actor, activity)
inboxes =
recipients
|> Enum.map(fn actor -> actor.inbox end)
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
Repo.checkout(fn ->
Enum.each(inboxes, fn {inbox, unreachable_since} ->
%User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
# Get all the recipients on the same host and add them to cc. Otherwise, a remote
# instance would only accept a first message for the first recipient and ignore the rest.
cc = get_cc_ap_ids(ap_id, recipients)
json =
data
|> Map.put("cc", cc)
|> Jason.encode!()
Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
inbox: inbox,
json: json,
actor_id: actor.id,
id: activity.data["id"],
unreachable_since: unreachable_since
})
end)
end)
end
# Publishes an activity to all relevant peers.
- def publish(%User{} = actor, %Activity{} = activity) do
+ def publish(%User{} = orig_actor, %Activity{} = activity) do
public = is_public?(activity)
if public && Config.get([:instance, :allow_relay]) do
Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
end
- {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, actor, activity, data} = prepare_activity(orig_actor, activity)
json = Jason.encode!(data)
recipients(actor, activity)
|> Enum.map(fn %User{} = user ->
determine_inbox(activity, user)
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
|> Enum.each(fn {inbox, unreachable_since} ->
Pleroma.Web.Federator.Publisher.enqueue_one(
__MODULE__,
%{
inbox: inbox,
json: json,
actor_id: actor.id,
id: activity.data["id"],
unreachable_since: unreachable_since
}
)
end)
end
def gather_webfinger_links(%User{} = user) do
[
%{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
%{
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"href" => user.ap_id
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => "#{Pleroma.Web.Endpoint.url()}/ostatus_subscribe?acct={uri}"
}
]
end
def gather_nodeinfo_protocol_names, do: ["activitypub"]
+
+ defp prepare_activity(orig_actor, activity) do
+ {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+
+ with {_, false} <- {:actor_changed?, data["actor"] != activity.data["actor"]} do
+ {:ok, orig_actor, activity, data}
+ else
+ {:actor_changed?, true} ->
+ # If prepare_outgoing changes the actor, re-get it from the db
+ actor = User.get_cached_by_ap_id(data["actor"])
+
+ activity = %Activity{activity | actor: actor.ap_id}
+
+ {:ok, actor, activity, data}
+ end
+ end
end
diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs
index c5137cbb7..9a9ec7e58 100644
--- a/test/pleroma/web/activity_pub/publisher_test.exs
+++ b/test/pleroma/web/activity_pub/publisher_test.exs
@@ -1,433 +1,477 @@
# 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.PublisherTest do
use Pleroma.Web.ConnCase
import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
import Mock
alias Pleroma.Activity
alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.CommonAPI
@as_public "https://www.w3.org/ns/activitystreams#Public"
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup_all do: clear_config([:instance, :federating], true)
describe "gather_webfinger_links/1" do
test "it returns links" do
user = insert(:user)
expected_links = [
%{"href" => user.ap_id, "rel" => "self", "type" => "application/activity+json"},
%{
"href" => user.ap_id,
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => "#{Pleroma.Web.Endpoint.url()}/ostatus_subscribe?acct={uri}"
}
]
assert expected_links == Publisher.gather_webfinger_links(user)
end
end
describe "determine_inbox/2" do
test "it returns sharedInbox for messages involving as:Public in to" do
user = insert(:user, %{shared_inbox: "http://example.com/inbox"})
activity = %Activity{
data: %{"to" => [@as_public], "cc" => [user.follower_address]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns sharedInbox for messages involving as:Public in cc" do
user = insert(:user, %{shared_inbox: "http://example.com/inbox"})
activity = %Activity{
data: %{"cc" => [@as_public], "to" => [user.follower_address]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns sharedInbox for messages involving multiple recipients in to" do
user = insert(:user, %{shared_inbox: "http://example.com/inbox"})
user_two = insert(:user)
user_three = insert(:user)
activity = %Activity{
data: %{"cc" => [], "to" => [user.ap_id, user_two.ap_id, user_three.ap_id]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns sharedInbox for messages involving multiple recipients in cc" do
user = insert(:user, %{shared_inbox: "http://example.com/inbox"})
user_two = insert(:user)
user_three = insert(:user)
activity = %Activity{
data: %{"to" => [], "cc" => [user.ap_id, user_two.ap_id, user_three.ap_id]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns sharedInbox for messages involving multiple recipients in total" do
user =
insert(:user, %{
shared_inbox: "http://example.com/inbox",
inbox: "http://example.com/personal-inbox"
})
user_two = insert(:user)
activity = %Activity{
data: %{"to" => [user_two.ap_id], "cc" => [user.ap_id]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns inbox for messages involving single recipients in total" do
user =
insert(:user, %{
shared_inbox: "http://example.com/inbox",
inbox: "http://example.com/personal-inbox"
})
activity = %Activity{
data: %{"to" => [user.ap_id], "cc" => []}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox"
end
end
describe "publish_one/1" do
test "publish to url with with different ports" do
inbox80 = "http://42.site/users/nick1/inbox"
inbox42 = "http://42.site:42/users/nick1/inbox"
mock(fn
%{method: :post, url: "http://42.site:42/users/nick1/inbox"} ->
{:ok, %Tesla.Env{status: 200, body: "port 42"}}
%{method: :post, url: "http://42.site/users/nick1/inbox"} ->
{:ok, %Tesla.Env{status: 200, body: "port 80"}}
end)
actor = insert(:user)
assert {:ok, %{body: "port 42"}} =
Publisher.publish_one(%{
inbox: inbox42,
json: "{}",
actor: actor,
id: 1,
unreachable_since: true
})
assert {:ok, %{body: "port 80"}} =
Publisher.publish_one(%{
inbox: inbox80,
json: "{}",
actor: actor,
id: 1,
unreachable_since: true
})
end
test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_reachable(inbox))
end
test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} =
Publisher.publish_one(%{
inbox: inbox,
json: "{}",
actor: actor,
id: 1,
unreachable_since: NaiveDateTime.utc_now()
})
assert called(Instances.set_reachable(inbox))
end
test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} =
Publisher.publish_one(%{
inbox: inbox,
json: "{}",
actor: actor,
id: 1,
unreachable_since: nil
})
refute called(Instances.set_reachable(inbox))
end
test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://404.site/users/nick1/inbox"
assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_unreachable(inbox))
end
test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://connrefused.site/users/nick1/inbox"
assert capture_log(fn ->
assert {:error, _} =
Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
end) =~ "connrefused"
assert called(Instances.set_unreachable(inbox))
end
test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
refute called(Instances.set_unreachable(inbox))
end
test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://connrefused.site/users/nick1/inbox"
assert capture_log(fn ->
assert {:error, _} =
Publisher.publish_one(%{
inbox: inbox,
json: "{}",
actor: actor,
id: 1,
unreachable_since: NaiveDateTime.utc_now()
})
end) =~ "connrefused"
refute called(Instances.set_unreachable(inbox))
end
end
describe "publish/2" do
test_with_mock "doesn't publish a non-public activity to quarantined instances.",
Pleroma.Web.Federator.Publisher,
[:passthrough],
[] do
Config.put([:instance, :quarantined_instances], [{"domain.com", "some reason"}])
follower =
insert(:user, %{
local: false,
inbox: "https://domain.com/users/nick1/inbox"
})
actor = insert(:user, follower_address: follower.ap_id)
{:ok, follower, actor} = Pleroma.User.follow(follower, actor)
actor = refresh_record(actor)
note_activity =
insert(:followers_only_note_activity,
user: actor,
recipients: [follower.ap_id]
)
res = Publisher.publish(actor, note_activity)
assert res == :ok
assert not called(
Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
inbox: "https://domain.com/users/nick1/inbox",
actor_id: actor.id,
id: note_activity.data["id"]
})
)
end
test_with_mock "Publishes a non-public activity to non-quarantined instances.",
Pleroma.Web.Federator.Publisher,
[:passthrough],
[] do
Config.put([:instance, :quarantined_instances], [{"somedomain.com", "some reason"}])
follower =
insert(:user, %{
local: false,
inbox: "https://domain.com/users/nick1/inbox"
})
actor = insert(:user, follower_address: follower.ap_id)
{:ok, follower, actor} = Pleroma.User.follow(follower, actor)
actor = refresh_record(actor)
note_activity =
insert(:followers_only_note_activity,
user: actor,
recipients: [follower.ap_id]
)
res = Publisher.publish(actor, note_activity)
assert res == :ok
assert called(
Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
inbox: "https://domain.com/users/nick1/inbox",
actor_id: actor.id,
id: note_activity.data["id"]
})
)
end
+ test_with_mock "Publishes with the new actor if prepare_outgoing changes the actor.",
+ Pleroma.Web.Federator.Publisher,
+ [:passthrough],
+ [] do
+ other_user =
+ insert(:user, %{
+ local: false,
+ inbox: "https://domain.com/users/nick1/inbox",
+ ap_enabled: true
+ })
+
+ actor = insert(:user)
+ replaced_actor = insert(:user)
+
+ note_activity =
+ insert(:note_activity,
+ user: actor,
+ data_attrs: %{"to" => [other_user.ap_id]}
+ )
+
+ with_mock Pleroma.Web.ActivityPub.Transmogrifier,
+ prepare_outgoing: fn data -> {:ok, Map.put(data, "actor", replaced_actor.ap_id)} end do
+ res = Publisher.publish(actor, note_activity)
+
+ assert res == :ok
+
+ refute called(
+ Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
+ inbox: "https://domain.com/users/nick1/inbox",
+ actor_id: actor.id,
+ id: note_activity.data["id"]
+ })
+ )
+
+ assert called(
+ Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
+ inbox: "https://domain.com/users/nick1/inbox",
+ actor_id: replaced_actor.id,
+ id: note_activity.data["id"]
+ })
+ )
+ end
+ end
+
test_with_mock "publishes an activity with BCC to all relevant peers.",
Pleroma.Web.Federator.Publisher,
[:passthrough],
[] do
follower =
insert(:user, %{
local: false,
inbox: "https://domain.com/users/nick1/inbox"
})
actor = insert(:user, follower_address: follower.ap_id)
user = insert(:user)
{:ok, follower, actor} = Pleroma.User.follow(follower, actor)
note_activity =
insert(:note_activity,
recipients: [follower.ap_id],
data_attrs: %{"bcc" => [user.ap_id]}
)
res = Publisher.publish(actor, note_activity)
assert res == :ok
assert called(
Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
inbox: "https://domain.com/users/nick1/inbox",
actor_id: actor.id,
id: note_activity.data["id"]
})
)
end
test_with_mock "publishes a delete activity to peers who signed fetch requests to the create acitvity/object.",
Pleroma.Web.Federator.Publisher,
[:passthrough],
[] do
fetcher =
insert(:user,
local: false,
inbox: "https://domain.com/users/nick1/inbox"
)
another_fetcher =
insert(:user,
local: false,
inbox: "https://domain2.com/users/nick1/inbox"
)
actor = insert(:user)
note_activity = insert(:note_activity, user: actor)
object = Object.normalize(note_activity, fetch: false)
activity_path = String.trim_leading(note_activity.data["id"], Pleroma.Web.Endpoint.url())
object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
build_conn()
|> put_req_header("accept", "application/activity+json")
|> assign(:user, fetcher)
|> get(object_path)
|> json_response(200)
build_conn()
|> put_req_header("accept", "application/activity+json")
|> assign(:user, another_fetcher)
|> get(activity_path)
|> json_response(200)
{:ok, delete} = CommonAPI.delete(note_activity.id, actor)
res = Publisher.publish(actor, delete)
assert res == :ok
assert called(
Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
inbox: "https://domain.com/users/nick1/inbox",
actor_id: actor.id,
id: delete.data["id"]
})
)
assert called(
Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
inbox: "https://domain2.com/users/nick1/inbox",
actor_id: actor.id,
id: delete.data["id"]
})
)
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 2, 9:21 AM (4 h, 3 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1804164
Default Alt Text
(24 KB)

Event Timeline