6c1ba76a3c445714d3d961870cb5bc60a6f44720
[anni] / lib / pleroma / web / activity_pub / publisher.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.Publisher do
6   alias Pleroma.Activity
7   alias Pleroma.Config
8   alias Pleroma.Delivery
9   alias Pleroma.HTTP
10   alias Pleroma.Instances
11   alias Pleroma.Object
12   alias Pleroma.Repo
13   alias Pleroma.User
14   alias Pleroma.Web.ActivityPub.Relay
15   alias Pleroma.Web.ActivityPub.Transmogrifier
16
17   require Pleroma.Constants
18
19   import Pleroma.Web.ActivityPub.Visibility
20
21   @behaviour Pleroma.Web.Federator.Publisher
22
23   require Logger
24
25   @moduledoc """
26   ActivityPub outgoing federation module.
27   """
28
29   @doc """
30   Determine if an activity can be represented by running it through Transmogrifier.
31   """
32   def is_representable?(%Activity{} = activity) do
33     with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
34       true
35     else
36       _e ->
37         false
38     end
39   end
40
41   @doc """
42   Publish a single message to a peer.  Takes a struct with the following
43   parameters set:
44
45   * `inbox`: the inbox to publish to
46   * `json`: the JSON message body representing the ActivityPub message
47   * `actor`: the actor which is signing the message
48   * `id`: the ActivityStreams URI of the message
49   """
50   def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
51     Logger.debug("Federating #{id} to #{inbox}")
52     uri = %{path: path} = URI.parse(inbox)
53     digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
54
55     date = Pleroma.Signature.signed_date()
56
57     signature =
58       Pleroma.Signature.sign(actor, %{
59         "(request-target)": "post #{path}",
60         host: signature_host(uri),
61         "content-length": byte_size(json),
62         digest: digest,
63         date: date
64       })
65
66     with {:ok, %{status: code}} = result when code in 200..299 <-
67            HTTP.post(
68              inbox,
69              json,
70              [
71                {"Content-Type", "application/activity+json"},
72                {"Date", date},
73                {"signature", signature},
74                {"digest", digest}
75              ]
76            ) do
77       if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do
78         Instances.set_reachable(inbox)
79       end
80
81       result
82     else
83       {_post_result, response} ->
84         unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
85         {:error, response}
86     end
87   end
88
89   def publish_one(%{actor_id: actor_id} = params) do
90     actor = User.get_cached_by_id(actor_id)
91
92     params
93     |> Map.delete(:actor_id)
94     |> Map.put(:actor, actor)
95     |> publish_one()
96   end
97
98   defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
99     if port == URI.default_port(scheme) do
100       host
101     else
102       "#{host}:#{port}"
103     end
104   end
105
106   defp should_federate?(inbox, public) do
107     if public do
108       true
109     else
110       %{host: host} = URI.parse(inbox)
111
112       quarantined_instances =
113         Config.get([:instance, :quarantined_instances], [])
114         |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
115         |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
116
117       !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
118     end
119   end
120
121   @spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
122   defp recipients(actor, activity) do
123     followers =
124       if actor.follower_address in activity.recipients do
125         User.get_external_followers(actor)
126       else
127         []
128       end
129
130     fetchers =
131       with %Activity{data: %{"type" => "Delete"}} <- activity,
132            %Object{id: object_id} <- Object.normalize(activity, fetch: false),
133            fetchers <- User.get_delivered_users_by_object_id(object_id),
134            _ <- Delivery.delete_all_by_object_id(object_id) do
135         fetchers
136       else
137         _ ->
138           []
139       end
140
141     Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
142   end
143
144   defp get_cc_ap_ids(ap_id, recipients) do
145     host = Map.get(URI.parse(ap_id), :host)
146
147     recipients
148     |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
149     |> Enum.map(& &1.ap_id)
150   end
151
152   defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox
153   defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox
154
155   @doc """
156   Determine a user inbox to use based on heuristics.  These heuristics
157   are based on an approximation of the ``sharedInbox`` rules in the
158   [ActivityPub specification][ap-sharedinbox].
159
160   Please do not edit this function (or its children) without reading
161   the spec, as editing the code is likely to introduce some breakage
162   without some familiarity.
163
164      [ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
165   """
166   def determine_inbox(
167         %Activity{data: activity_data},
168         %User{inbox: inbox} = user
169       ) do
170     to = activity_data["to"] || []
171     cc = activity_data["cc"] || []
172     type = activity_data["type"]
173
174     cond do
175       type == "Delete" ->
176         maybe_use_sharedinbox(user)
177
178       Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
179         maybe_use_sharedinbox(user)
180
181       length(to) + length(cc) > 1 ->
182         maybe_use_sharedinbox(user)
183
184       true ->
185         inbox
186     end
187   end
188
189   @doc """
190   Publishes an activity with BCC to all relevant peers.
191   """
192
193   def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
194       when is_list(bcc) and bcc != [] do
195     public = is_public?(activity)
196     {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
197
198     recipients = recipients(actor, activity)
199
200     inboxes =
201       recipients
202       |> Enum.filter(&User.ap_enabled?/1)
203       |> Enum.map(fn actor -> actor.inbox end)
204       |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
205       |> Instances.filter_reachable()
206
207     Repo.checkout(fn ->
208       Enum.each(inboxes, fn {inbox, unreachable_since} ->
209         %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
210
211         # Get all the recipients on the same host and add them to cc. Otherwise, a remote
212         # instance would only accept a first message for the first recipient and ignore the rest.
213         cc = get_cc_ap_ids(ap_id, recipients)
214
215         json =
216           data
217           |> Map.put("cc", cc)
218           |> Jason.encode!()
219
220         Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
221           inbox: inbox,
222           json: json,
223           actor_id: actor.id,
224           id: activity.data["id"],
225           unreachable_since: unreachable_since
226         })
227       end)
228     end)
229   end
230
231   # Publishes an activity to all relevant peers.
232   def publish(%User{} = actor, %Activity{} = activity) do
233     public = is_public?(activity)
234
235     if public && Config.get([:instance, :allow_relay]) do
236       Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end)
237       Relay.publish(activity)
238     end
239
240     {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
241     json = Jason.encode!(data)
242
243     recipients(actor, activity)
244     |> Enum.filter(fn user -> User.ap_enabled?(user) end)
245     |> Enum.map(fn %User{} = user ->
246       determine_inbox(activity, user)
247     end)
248     |> Enum.uniq()
249     |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
250     |> Instances.filter_reachable()
251     |> Enum.each(fn {inbox, unreachable_since} ->
252       Pleroma.Web.Federator.Publisher.enqueue_one(
253         __MODULE__,
254         %{
255           inbox: inbox,
256           json: json,
257           actor_id: actor.id,
258           id: activity.data["id"],
259           unreachable_since: unreachable_since
260         }
261       )
262     end)
263   end
264
265   def gather_webfinger_links(%User{} = user) do
266     [
267       %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
268       %{
269         "rel" => "self",
270         "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
271         "href" => user.ap_id
272       },
273       %{
274         "rel" => "http://ostatus.org/schema/1.0/subscribe",
275         "template" => "#{Pleroma.Web.Endpoint.url()}/ostatus_subscribe?acct={uri}"
276       }
277     ]
278   end
279
280   def gather_nodeinfo_protocol_names, do: ["activitypub"]
281 end