Merge remote-tracking branch 'upstream/develop' into accept-deletes
[akkoma] / lib / pleroma / web / activity_pub / publisher.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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 %{host: host, path: path} = URI.parse(inbox)
53
54 digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
55
56 date = Pleroma.Signature.signed_date()
57
58 signature =
59 Pleroma.Signature.sign(actor, %{
60 "(request-target)": "post #{path}",
61 host: host,
62 "content-length": byte_size(json),
63 digest: digest,
64 date: date
65 })
66
67 with {:ok, %{status: code}} when code in 200..299 <-
68 result =
69 HTTP.post(
70 inbox,
71 json,
72 [
73 {"Content-Type", "application/activity+json"},
74 {"Date", date},
75 {"signature", signature},
76 {"digest", digest}
77 ]
78 ) do
79 if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
80 do: Instances.set_reachable(inbox)
81
82 result
83 else
84 {_post_result, response} ->
85 unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
86 {:error, response}
87 end
88 end
89
90 def publish_one(%{actor_id: actor_id} = params) do
91 actor = User.get_cached_by_id(actor_id)
92
93 params
94 |> Map.delete(:actor_id)
95 |> Map.put(:actor, actor)
96 |> publish_one()
97 end
98
99 defp should_federate?(inbox, public) do
100 if public do
101 true
102 else
103 %{host: host} = URI.parse(inbox)
104
105 quarantined_instances =
106 Config.get([:instance, :quarantined_instances], [])
107 |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
108
109 !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
110 end
111 end
112
113 @spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
114 defp recipients(actor, activity) do
115 followers =
116 if actor.follower_address in activity.recipients do
117 User.get_external_followers(actor)
118 else
119 []
120 end
121
122 fetchers =
123 with %Activity{data: %{"type" => "Delete"}} <- activity,
124 %Object{id: object_id} <- Object.normalize(activity),
125 fetchers <- User.get_delivered_users_by_object_id(object_id),
126 _ <- Delivery.delete_all_by_object_id(object_id) do
127 fetchers
128 else
129 _ ->
130 []
131 end
132
133 Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
134 end
135
136 defp get_cc_ap_ids(ap_id, recipients) do
137 host = Map.get(URI.parse(ap_id), :host)
138
139 recipients
140 |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
141 |> Enum.map(& &1.ap_id)
142 end
143
144 defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox
145 defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox
146
147 @doc """
148 Determine a user inbox to use based on heuristics. These heuristics
149 are based on an approximation of the ``sharedInbox`` rules in the
150 [ActivityPub specification][ap-sharedinbox].
151
152 Please do not edit this function (or its children) without reading
153 the spec, as editing the code is likely to introduce some breakage
154 without some familiarity.
155
156 [ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
157 """
158 def determine_inbox(
159 %Activity{data: activity_data},
160 %User{inbox: inbox} = user
161 ) do
162 to = activity_data["to"] || []
163 cc = activity_data["cc"] || []
164 type = activity_data["type"]
165
166 cond do
167 type == "Delete" ->
168 maybe_use_sharedinbox(user)
169
170 Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
171 maybe_use_sharedinbox(user)
172
173 length(to) + length(cc) > 1 ->
174 maybe_use_sharedinbox(user)
175
176 true ->
177 inbox
178 end
179 end
180
181 @doc """
182 Publishes an activity with BCC to all relevant peers.
183 """
184
185 def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
186 when is_list(bcc) and bcc != [] do
187 public = is_public?(activity)
188 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
189
190 recipients = recipients(actor, activity)
191
192 inboxes =
193 recipients
194 |> Enum.filter(&User.ap_enabled?/1)
195 |> Enum.map(fn actor -> actor.inbox end)
196 |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
197 |> Instances.filter_reachable()
198
199 Repo.checkout(fn ->
200 Enum.each(inboxes, fn {inbox, unreachable_since} ->
201 %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
202
203 # Get all the recipients on the same host and add them to cc. Otherwise, a remote
204 # instance would only accept a first message for the first recipient and ignore the rest.
205 cc = get_cc_ap_ids(ap_id, recipients)
206
207 json =
208 data
209 |> Map.put("cc", cc)
210 |> Jason.encode!()
211
212 Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
213 inbox: inbox,
214 json: json,
215 actor_id: actor.id,
216 id: activity.data["id"],
217 unreachable_since: unreachable_since
218 })
219 end)
220 end)
221 end
222
223 @doc """
224 Publishes an activity to all relevant peers.
225 """
226 def publish(%User{} = actor, %Activity{} = activity) do
227 public = is_public?(activity)
228
229 if public && Config.get([:instance, :allow_relay]) do
230 Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end)
231 Relay.publish(activity)
232 end
233
234 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
235 json = Jason.encode!(data)
236
237 recipients(actor, activity)
238 |> Enum.filter(fn user -> User.ap_enabled?(user) end)
239 |> Enum.map(fn %User{} = user ->
240 determine_inbox(activity, user)
241 end)
242 |> Enum.uniq()
243 |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
244 |> Instances.filter_reachable()
245 |> Enum.each(fn {inbox, unreachable_since} ->
246 Pleroma.Web.Federator.Publisher.enqueue_one(
247 __MODULE__,
248 %{
249 inbox: inbox,
250 json: json,
251 actor_id: actor.id,
252 id: activity.data["id"],
253 unreachable_since: unreachable_since
254 }
255 )
256 end)
257 end
258
259 def gather_webfinger_links(%User{} = user) do
260 [
261 %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
262 %{
263 "rel" => "self",
264 "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
265 "href" => user.ap_id
266 },
267 %{
268 "rel" => "http://ostatus.org/schema/1.0/subscribe",
269 "template" => "#{Pleroma.Web.base_url()}/ostatus_subscribe?acct={uri}"
270 }
271 ]
272 end
273
274 def gather_nodeinfo_protocol_names, do: ["activitypub"]
275 end