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